Testing GraphQL Subscriptions

GraphQL subscriptions are a relatively new “feature”. Although there are already a couple of tutorials on how to use it (i.e. https://dev-blog.apollodata.com/graphql-subscriptions-in-apollo-client-9a2457f015fb, https://blog.graph.cool/how-to-build-a-real-time-chat-with-graphql-subscriptions-and-apollo-d4004369b0d4, etc), I was not able to find any of explaining how to “test” them.

Seems like no body does TDD anymore these days :P

So this brief post is like a snippet on how to test a subscription using mocha/chai, like in an “integration-test”

What we are going to test is the Server part of the subscriptions, and not the client (browser).

The API

In this case the Graph API has

  • an Object type: which are like generic entities of the system. Each object belongs to a “project”, which is another concept of the domain. They are stored in a mongo database through mongoose.
  • an updateObject(project, objectId, property, value) mutation: used to update a property on one of those objects.
  • an objectUpdated(project:String) subscription: used to subscribe to events on any object of the given project.

This means that when a client performs am “updateObject” mutation the server will trigger an event for subscriptions on that same project.

type Object {

project: String
id: Int!
properties: JSON // kind of a mixed type
}
Mutation: {

updateObject(input: UpdateObjectInput) : Object
}
input UpdateObjectInput {

project: String!
id: Int!
property: String!
value: String!
}
Subscription: {
    objectUpdated(project: String!): ObjectPropertyChanged
}
type ObjectPropertyChanged {

project: String!
id: Int!
property: String!
value: String!
}

The resolvers code is not important at this point nor any other Query

Testing: first test, mutation

First lets do a test for the mutation alone. This is the final test with some “magic”

describeGQL('GRAPHQL API - Objects()', ({ expectQueryResult }) => {
  beforeEach('setup objects in mongo', async () => {
await Object.insertMany([
{ project: 'game', id: 10, data: { name: 'Pato Bullrich' } }
])
})
  it('objectUpdate() updates an Object', async () => {
await expectQueryResult(

// MUTATION
`mutation updateGame10 {
updateBNEObject(input: {
project: "game",
id: 10,
property: "name",
value: "Donde esta Santiago Maldonado?"
}) {
id
sys
data
}
}`,
// EXPECTED RESPONSE
{
updateGame10: {
id: 10,
sys: 'marker',
data: { name: 'Donde esta Santiago Maldonado?' }
}
}
)
    // Assert modified in mongo
const updated = await Object.findOne({ project: 'game', id: 10 })
expect(modifiedBNEObject.data.name).to.equal('sarasa')
})
})

The magic here is in `expectQueryResult()` which already encapsulates a lot of complexity under the form of a function similar to an “equals” assert.

The signature is

 expectQueryResult( query, expectedResponse)

Now this function gets “injected” as part of the body of the test, if you look at the first line, because we are not using the regular “describe()” method but a “describeGQL()` function

describeGQL("some text", ({ expectQueryResult }) => {
      // tests here using expectQueryResult
})

describeGQL hides away a lot of setup to reuse between different tests

describeGQL(): test “infrastructure”

This function follows kind of a “pattern” that I follow when I found myself doing the same code in many different “tests” in mocha. For example all express routes tests, or all tests that require mongo, etc. They usually have some before(), after(), beforeEach(), afterEach() plus all the required imports.

The way to solve this is to create like a “describe” replacement or function that wraps an original describe, and can be used as a regular describe (meaning **describe.skip** and **describe.only**)

So it uses high-order function for that

import { graphql } from 'graphql'
import mongoose from 'mongoose'
import { Mockgoose } from 'mockgoose'
import '../../src/models/all'
import { expect } from 'chai'
import { compileSchemas } from '../../src/graphql/utils/compileSchema'


const internalDescribeQL = (describeFn) => (title, body) => {
describeFn(title, () => {
let schema

const mockgoose = new Mockgoose(mongoose);

before(async () => {
await mockgoose.prepareStorage()
await mongoose.connect('mongodb://example.com/TestingDB')

schema = compileSchemas()
})

after(done => {
mongoose.disconnect(done)
})

beforeEach(() => {
mockgoose.helper.reset()
})

const expectQuery = async(query, expected) => {
expect(await graphql(schema, query, undefined, { })).to.deep.equal(expected)
}

const expectQueryResult = (query, expected) => {
return expectQuery(query, { data: expected })
}

body({ expectQuery, expectQueryResult }, schema)

})
}

export const describeGQL = internalDescribeQL(describe)
describeGQL.only = internalDescribeQL(describe.only)
describeGQL.skip = internalDescribeQL(describe.skip)

This has some specifics from the project like where the mongoose models are, or how to build the schema. If you follow any other graphql tutorial you will end up with the schema, that is the only thing that you need here.

Notice that the wrapper ends up calling the “body” function, that is your code within the describe() block, sending an object with some parameters.

This object has basically all the utility methods to do graphql expectations/asserts. As they require the connection and context they cannot be just functions out there.

So this is how the test receives the **expectQueryResult()** function :)

Finally: Testing Subscription

So to test subscriptions we need to change the **describeGQL()** function because it needs some setup:

  • Start the backend subscriptions server (websockets)
  • Create a graphql-subscription client connecting to the server (we will use apollo-client here on the server, just for test)
  • Provide a way for the test to subscribe

First install some libraries for this

yarn install apollo-client graphql-tag ws

Now lets change describeGQL() by adding some imports and constants

import WebSocket from 'ws'
import { SubscriptionClient } from 'subscriptions-transport-ws';
import ApolloClient from 'apollo-client'
const GRAPHQL_ENDPOINT = 'ws://localhost:5000/subscriptions';

Then in the before() start the server and create an apollo client

describeFn(title, () => {
// context
let schema
let apollo // <<< gets sets in the before()
let networkInterface // <<< same
  before(async () => {
// .. old stuff here
    // start server
await rtm.start() // << this depends on your server
    // connect client    
networkInterface = new SubscriptionClient(GRAPHQL_ENDPOINT, { reconnect: true }, WebSocket)
apollo = new ApolloClient({ networkInterface })
})

And we need to shut it down on the after

  after(done => {
networkInterface.close() // stop client
rtm.shutdown() // << stop server

// ... some others
})

And finally we will make “apollo” available for the test body function.

const client = () => apollo
// interpret body
body({ expectQuery, expectQueryResult, execGraphQL, client }, schema)

So now the we can add a test

it('objectUpdated receives an update from calling mutation updateObject() in the same project', async () => {
    // SUBSCRIBE and make a promise
const subscriptionPromise = new Promise((resolve, reject) => {
client().subscribe({
query: gql`
subscription objectUpdated {
objectUpdated(project: "game") {
id
property
value
}
}`
}).subscribe({
next: resolve,
error: reject
})
})

// MUTATE
await execGraphQL(
`mutation updateBNEObject {
updateObject(input: { project: "game", id: 10, property: "name", value: "Donde esta Santiago Maldonado?"}) {
id
}
}`
)

// ASSERT SUBSCRIPTION RECEIVED EVENT
expect(await subscriptionPromise).to.deep.equal({
objectUpdated: {
__typename: 'ObjectPropertyChanged',
id: 10,
property: 'name',
value: 'Donde esta Santiago Maldonado?'
}
})
})
})

In the first part we use the “client” object to subscribe to a subscription query. That gives us an Observable for which we register an observer that just adapts it to a promise. When it receives data, it resolves the promise. If there is an error, then rejects it.

Then we call a mutation to simulate someone changing and object.

Finally we wait for the subscription promise and assert that it receives the correct objects.

And that’s it.

Or not.. this code can be refactor. I don’t like the subscription being a promise. To make it scale it would be good to have some kind of generic “subscriptor” or listener to which one can ask for all the events that it received. Kind of redux mockstore.getActions().

But this is enough for today :)

I hope this helped a little bit since I couldn’t find any clear documentation on how to do this tests.

Annex

Here is the complete describeGQL() function

import { graphql } from 'graphql'
import mongoose from 'mongoose'
import { Mockgoose } from 'mockgoose'
import rtm from '../../src/features/rtm'
import '../../src/models/all'
import { expect } from 'chai'
import { compileSchemas } from '../../src/graphql/utils/compileSchema'

// subscriptiosn
import WebSocket from 'ws'
import { SubscriptionClient } from 'subscriptions-transport-ws';
import ApolloClient from 'apollo-client'

// TODO: take url from config
const GRAPHQL_ENDPOINT = 'ws://localhost:5000/subscriptions';

const internalDescribeQL = (describeFn) => (title, body) => {
describeFn(title, () => {
// context
let schema
let apollo
let networkInterface

const mockgoose = new Mockgoose(mongoose);

before(async () => {
await mockgoose.prepareStorage()
await mongoose.connect('mongodb://example.com/TestingDB')

schema = compileSchemas()

await rtm.setup({})

// subscriptions
networkInterface = new SubscriptionClient(GRAPHQL_ENDPOINT, { reconnect: true }, WebSocket)
apollo = new ApolloClient({ networkInterface })
})

after(done => {
rtm.teardown()
networkInterface.close()
mongoose.disconnect(done)
})

beforeEach(() => {
mockgoose.helper.reset()
})

// utils functions for tests
const expectQuery = async(query, expected) =>
expect(await execGraphQL(query)).to.deep.equal(expected)

const expectQueryResult = (query, expected) => expectQuery(query, { data: expected })

const execGraphQL = query => graphql(schema, query, undefined, { })
const client = () => apollo

// interpret body
body({ expectQuery, expectQueryResult, execGraphQL, client }, schema)

})
}

export const describeGQL = internalDescribeQL(describe)
describeGQL.only = internalDescribeQL(describe.only)
describeGQL.skip = internalDescribeQL(describe.skip)