1. Previously on this blog
In part 1 of this little series, we looked at how we can set up GraphQL subscriptions: the “vanilla” ones that are automatically triggered by a mutation as well as manually triggered subscriptions. In this post, we will write some tests for the former kind. Let’s go!
2. The infrastructure
First of all, let’s have a quick look into our test folder:
├── test
│ ├── my_app
│ │ ├── some_other_test.exs
│ ├── my_app_web
│ │ ├── some_other_test.exs
│ │ ├── schema
│ │ │ ├── mutations
│ │ │ │ ├── some_mutation_test.exs
│ │ │ │ └── user_test.exs
│ │ │ ├── queries
│ │ │ │ ├── some_query_test.exs
│ │ │ │ └── user_test.exs
│ │ │ └── subscriptions
│ │ │ └── user_test.exs
│ │ └── views
│ │ └── error_view_test.exs
│ ├── support
│ │ ├── channel_case.ex
│ │ ├── conn_case.ex
│ │ ├── data_case.ex
│ │ ├── fixtures
│ │ │ └── user_fixtures.ex
│ │ └── subscription_case.ex
│ └── test_helper.exs
We see that, as per usual, the structure of our test
folder mirrors the lib
folder (shown in the previous post).
In the support
folder, we want to make sure that @endpoint
in channel_case.ex
is set correctly (it should be by default).
The ChannelCase
module imports Phoenix.ChannelTest
, which provides the utility functions for pushing and receiving messages within ExUnit
. In our case, the module would look like this:
defmodule MyAppWeb.ChannelCase do
use ExUnit.CaseTemplate
alias Ecto.Adapters.SQL.Sandbox
using do
quote do
# Import conveniences for testing with channels
import Phoenix.ChannelTest
import MyAppWeb.ChannelCase
# The default endpoint for testing
@endpoint MyAppWeb.Endpoint
end
end
setup tags do
pid = Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
on_exit(fn -> Sandbox.stop_owner(pid) end)
:ok
end
end
One file that is not autogenerated and that we need to add to the support
folder is subscription_case.ex
.
Here is what it looks like:
defmodule MyAppWeb.SubscriptionCase do
@moduledoc """
This module defines the test case to be used by
subscription tests.
"""
use ExUnit.CaseTemplate
alias Absinthe.Phoenix.SubscriptionTest
alias Phoenix.ChannelTest
using do
quote do
use MyAppWeb.ChannelCase
use Absinthe.Phoenix.SubscriptionTest, schema: MyAppWeb.Schema
setup do
{:ok, socket} = ChannelTest.connect(MyAppWeb.UserSocket, %{})
{:ok, socket} = SubscriptionTest.join_absinthe(socket)
{:ok, %{socket: socket}}
end
end
end
end
We can see that our SubscriptionCase
uses both ChannelCase
and Absinthe.Phoenix.SubscriptionTest
. This allows us to push Absinthe docs up to Absinthe for Absinthe to reply. In the setup
block - which, in this case, needs to be within the use
block - we connect our UserSocket
to a channel and subsequently set up Absinthe on that socket using join_absinthe/1
. With our SubscriptionCase
in place, we can now go ahead and write the tests.
3. The test
Here is what a test for a basic, mutation-triggered subscription looks like:
defmodule MyAppWeb.Schema.Subscriptions.UserTest do
use MyAppWeb.SubscriptionCase
@create_user_doc """
mutation CreateUser($name: String!, $email: String!){
createUser (name: $name, email: $email) {
id
name
email
}
}
"""
@created_user_doc """
subscription CreatedUser {
createdUser {
id
name
email
}
}
"""
describe "@created_user" do
test "sends a user when @createdUser mutation is triggered", %{socket: socket} do
ref = push_doc(socket, @created_user_doc, variables: %{})
assert_reply ref, :ok, %{subscriptionId: subscription_id}
ref =
push_doc(socket, @create_user_doc,
variables: %{
"name" => "Waldo",
"email" => "butters@example.com",
"preferences" => @preferences
}
)
assert_reply ref, :ok, reply
assert %{
data: %{
"createUser" => %{
"name" => "Waldo",
"email" => "butters@example.com"
}
}
} = reply
assert_push "subscription:data", data
assert %{
subscriptionId: ^subscription_id,
result: %{
data: %{
"createdUser" => %{
"name" => "Waldo",
"email" => "butters@example.com"
}
}
}
} = data
end
end
end
As we can see in the example above, we start by building ‘docs’ (documents) both for the mutation we are subscribed to and for the corresponding subscription. When we write these docs, we are building the same query that our GraphiQL interface would build based on our input. We are specifying the arguments and types in the first line of the doc. In the second line, we have the name of our mutation or subscription and the arguments again, and below that is what the mutation/subscription returns.
By passing the docs into push_doc/3
later on in the subscription test (or, likewise, when we pass a doc into Absinthe.run/3
in a mutation or query test), we utilize the internal API that Absinthe uses, thus bypassing the router and increasing test performance.
In the actual test, we build the socket by passing in the %{socket: socket}
map. We then push the subscription doc to the socket and assert that the subscription ID is returned.
Next up, we push our mutation to the socket and assert that we get an :ok
and a reply
as well as that the reply contains the data with which we created the user in our create_user
mutation.
NOTE: Unlike Absinthe.run/3
, push_doc/3
will only pass through whatever is under the variables
key in the third argument (opts
). In other words, if, in your mutation test, you are passing information on under the context
key in your Absinthe.run/3
function (e.g. a secret key for authorization via an HTTP header), you will have to find a workaround in your subscription test. I will write more about that in the next post.
But back to our example test, where there are only two steps left:
First, we need to assert that data was pushed to the client and, last but not least, we assert that the data matches the data that we pushed in our create_user
mutation and that the subscription_id
we got back matches the subscription_id
we got back when we first pushed our @created_user_doc
.
And that’s it!