Testing Absinthe with ExUnit

This is how I tested some GraphQL endpoints using ExUnit

I’ve been trying to learn more Elixir recently and I’ve had reason to be looking at Absinthe, which is used to get GraphQL functionality in Elixir.

Something I couldn’t find a definitive best practice on was how best to test the various endpoints you end up creating for GraphQL to consume.

This article is my stab at writing some Absinthe tests. The huge caveat being that I am still very much learning all technologies involved. If you have any opinions on how the following could be improved please do get in touch.

How GraphQL likes to receive query data

In a RESTful interface you could do something like

GET /notes/1

And expect to get back some data about a Note with an ID of 1.

With GraphQL you would send something like

{
  note(id: 1) {
    title
    author_id
  }
}

This is pretty awesome, because instead of knowing you will get back something about a Note now we are explicitly stating we want the title and the author_id.

How that looks like when we make a web request is something like this

{"query":"{\n  note(id: 1) {\n    title\n author_id\n  }\n}","variables":null,"operationName":null}

Pretty ugly, but you will notice there is some extra information in this request, our query is wrapped in a query attribute and there are two other attributes in this object; variables and operationName.

This is important because when we’re testing our endpoints we will need to make sure we send the same type of data.

The result of this call will look like this

{
  data: {
    note: {
      id: 1
    }
  }
}

How GraphQL likes to receive mutation data

In GraphQL when we want to create or update something we do what it calls a mutation.

This looks something like

mutation {
  createNote(title: "test") {
    title
  }
}

This will create a Note with the title “test” and with the object it creates return the title attribute to me.

Again this is really nice because often when we create an object we only care about a small subset of the information used to created in our application.

The web request for the above mutation looks like this

{"query":"mutation {\n  createNote(title: \"test\") {\n    title\n  }\n}","variables":null,"operationName":null}

Here we see again those three attributes; query, variables, and operationName.

Lets test Absinthe

Okay, enough preamble, lets test something!

I will share the three relevant files and under each I will walk through what I’ve included and done and why.

# test/web/resolver/note_resolver_test.exs
defmodule MyApp.NoteResolverTest do
  use MyApp.ConnCase
  alias MyApp.Notes
  alias MyApp.AbsintheHelpers

  @note %{body: "some body", deleted: true, faved: true, title: "some title"}

  describe "Note Resolver" do
    test "find/2 returns a note", context do
      {:ok, note} = Notes.create_note(@note)

      query = """
      {
        note(id: #{note.id}) {
          id
        }
      }
      """

      res = context.conn
        |> post("/graphiql", AbsintheHelpers.query_skeleton(query, "notes"))

      assert json_response(res, 200)["data"]["note"]["id"] == to_string(note.id)
    end
  end
end
  • I want to exercise our Note resolver, so I’ve created a NoteResolverTest module.
  • I use MyApp.ConnCase which sets up the CaseTemplate I want to use for these tests. This sets up some connection information, more on this shortly.
  • I alias MyApp.Notes because I need access to the Notes module later.
  • I alias MyApp.AbsintheHelpers to get access to some helpers I have written, more on this shortly
  • Setting the @note map is just a convenience for later in my test file.
  • I set up a describe block for “Note Resolver”, this is where I will put my tests.
  • We have one test in here, rather poorly named “find/2 returns a note”, we pass in a context which is set in my CaseTemplate.
  • The {:ok, note} = Notes.create_note(@note) line just creates a note in our system, how create_note/1 works is unimportant for this article.
  • We then construct our query as a string, note we interpolate in the note.id that we care about.
  • Next we take our connection and perform a post to our endpoint (in this case /graphiql) and we pass in the result of AbsintheHelpers.query_skeleton/2, more on this shortly.
  • Now we’ve made the call we can assert based on the response, we dig into the json_response into “data”, and “note” to get to “id” which we then compare to our note.id which we cast to a String.

At a high level I think this makes sense, we set up our database, we construct our query, we execute and then we compare results.

Lets look now at the relevant parts of MyApp.ConnCase, which sets the connection we use to make our post request.

# test/support/conn_case.ex
setup do
  {:ok, %{token: token}} = Accounts.sign_up(@user)
  conn = Phoenix.ConnTest.build_conn()
    |> Plug.Conn.put_req_header("authorization", "Bearer #{token}")
    |> Plug.Conn.put_req_header("content-type", "application/json")

  {:ok, %{conn: conn}}
end
  • We do this inside a setup block so that it runs before each of our tests.
  • In this setup our requests need authenticated, so we sign_up a user and store their authentication token, how this happens isn’t important for this article.
  • We then build a connection and set two headers, the first is our authorization header, that sets our token. The second is content-type which specifies that it will be sending JSON.
  • Finally, we return {:ok, %{conn: conn}} so that we can have access to the connection from our tests.

Finally, lets look at our AbsintheHelpers

# test/support/absinthe_helpers.ex
defmodule MyApp.AbsintheHelpers do
  def query_skeleton(query, query_name) do
    %{
      "operationName" => "#{query_name}",
      "query" => "query #{query_name} #{query}",
      "variables" => "{}"
    }
  end
end
  • I wanted to abstract this out into its own module so I could use them in several tests, so I made AbsintheHelpers, this name could be improved.
  • query_skeleton/2 accepts two parameters, our query and a name we want to give our query.
  • This function returns a map just like we saw how GraphQL likes to receive data.

You will note that when setting the query attribute I’ve started the string with the word “query”, this wasn’t in our earlier example but tests fail when I don’t do this, I’m not sure why. If you know please do comment!

Testing Mutations

Mutation testing work almost the exact same, you construct your mutation and send it and then test for what gets returned or set in the database. The big difference is unlike with queries were things broke if I didn’t start the string with the word “query”, mutations need to not have that.

This leads me to my second function in AbsintheHelpers

def mutation_skeleton(query) do
  %{
    "operationName" => "",
    "query" => "#{query}",
    "variables" => ""
  }
end

This simple function generates a map with just the query and no additional text. An example query would be

mutation {
  createNote(title: “lol”) {
    title
  }
}

Thanks

The only bit of information I could find on this was a post over on the Elixir Forum, without which I would have stayed lost for a long time.

Thanks for reading and hopefully this was helpful.

Recent posts View all

Freelancing

Getting the most out of your agency

Here are some tips based on years of working with, for, and as an agency on how to get the most out of any work you do.

VS Code Web Dev

Select multiple lines in VS Code

How to select multiple lines to edit at once within VS Code