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.
Let’s test Absinthe
Okay, enough preamble, let’s 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
NoteResolverTestmodule. - I
use MyApp.ConnCasewhich sets up theCaseTemplateI want to use for these tests. This sets up some connection information, more on this shortly. - I
alias MyApp.Notesbecause I need access to theNotesmodule later. - I
alias MyApp.AbsintheHelpersto get access to some helpers I have written, more on this shortly - Setting the
@notemap 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, howcreate_note/1works is unimportant for this article. - We then construct our
queryas a string, note we interpolate in thenote.idthat we care about. - Next we take our connection and perform a
postto our endpoint (in this case/graphiql) and we pass in the result ofAbsintheHelpers.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_responseinto “data”, and “note” to get to “id” which we then compare to ournote.idwhich 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.
Let’s 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
setupblock so that it runs before each of our tests. - In this setup our requests need authenticated, so we
sign_upa user and store their authenticationtoken, how this happens isn’t important for this article. - We then build a connection and set two headers, the first is our
authorizationheader, that sets our token. The second iscontent-typewhich 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, let’s 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/2accepts 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.