GrapQL subscriptions with Absinthe - testing basic subscriptions
2022-10-19
Written by Cornelia Kelinske

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!