How to test graphql subscriptions

augustocalaca

Augusto Calaca

Posted on June 15, 2020

How to test graphql subscriptions

Just write code isn't enough. We need to test it. The benefits this habit in programming are many ranging from ensure that the code work as designed to gain valuable time when refactoring your codebase.

Before testing we need to understand some things.

The subscribe execute has that signature besides having other fields that we won't need to this example:

// graphql-js

export function subscribe(
  schema: GraphQLSchema,
  document: DocumentNode,
  rootValue?: any,
  contextValue?: any,
  variableValues?: Maybe<{ [key: string]: any }>,
): Promise<AsyncIterableIterator<ExecutionResult> | ExecutionResult>;

Fist our schema with the required definitions, second our graphlq tag string which we will to use the parse function to turn it into a DocumentNode, followed by the rootValue explained later, the contextValue and lastly the variables required by our second argument.
The subscribe execute return a object with three promises: next(), return() and throw(). Let's just stick to the next() function which return our subscription fields once it is performed.

"But where and how do I trigger my subscription?" you may wonder.
Lets us understand the subscribe rootValue argument. The root value represents the "top" of your metaphorical graph of data, and is useful to include functions or data to help resolve the root fields in your schema as Lee Byron explains very well in this comment. That's where we're gonna put our trigger to test our subscription.
The trigger is a PubSub that must be somewhere of the our mutation. Something like:

// UserAddMutation.ts

...
await pubSub.publish(EVENTS.USER.ADDED, { userId: user._id });
...

As soon as this event is triggered all the subscriptions prepared to listen to it will receive the userId value which is the shape of data that should be sent. See the code below:

// UserAddedSubscription.ts

import { subscriptionWithClientId } from 'graphql-relay-subscription';

import UserType from '../UserType';
import * as UserLoader from '../UserLoader';
import pubSub, { EVENTS } from '../../../channels/pubSub';

const UserAddedSubscription = subscriptionWithClientId({
  ...
  subscribe: () => pubSub.asyncIterator(EVENTS.USER.ADDED),
  getPayload: (root) => {
    return { id: root.userId };
  },
  outputFields: {
    user: {
      type: UserType,
      resolve: (root, _, context) => UserLoader.load(context, root.id),
    },
  },
});

export default UserAddedSubscription;

Great, our subscription is interested this event named EVENT.USER.ADDED being able to listen to it and to receive the userId value through the root field on getPayload function. Finally, the getPayload sends this data to outputFields and resolve the user type.

Notice that for our event to be published our mutation must be executed. In that case, we're gonna put all graphql mutation on rootValue of the subscription like this:

// UserAddedSubscription.spec.ts
...
const contextValue = await getContext({});
const variablesMutation = {
  input: {
    name: 'Awesome Name',
    username: 'awesomeusername',
    email: 'awesome@email.com',
    password: '12345',
  },
};
const variablesSubscription = {
  input: {},
};

// focus here
const triggerSubscription = graphql(schema, mutation, rootValue, contextValue, variablesMutation);
const result = await subscribe(schema, parse(subscription), triggerSubscription, contextValue, variablesSubscription);
...

Notice the parse function and your role on grahql-js.
See also the variablesSubscription and realize that it automatically adds a field named clientSubscriptionId.

Below is the data that we want to receive when our subscription to execute:

// UserAddedSubscription.spec.ts

const subscription = `
  subscription S($input: UserAddedInput!) {
    UserAdded(input: $input) {
      user {
        name
        username
        email
        isActive
        followers {
          totalCount
        }
        following {
          totalCount
        }
      }
    }
  }
`;

In the stretch ahead we checked if the received values by the next() function match the expected values. Realise that the name and username fields are the same as fields passed to variablesMutation so our subscription is receiving the User created at the time of the mutation in real-time.

// UserAddedSubscription.spec.ts
...
expect((await result.next()).value.data).toEqual({
  UserAdded: {
    user: {
      name: 'Awesome Name',
      username: 'awesomeusername',
      email: null,
      isActive: true,
      followers: {
        totalCount: 0,
      },
      following: {
        totalCount: 0,
      },
    },
  }
});
...

The complete code you can see in theses gists: UserAddedSubscription.spec.ts and UserAddedSubscription.ts

💖 💪 🙅 🚩
augustocalaca
Augusto Calaca

Posted on June 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related