Testing a GraphQL Server using Jest

I've created this boilerplate to make it easy to start a new GraphQL project using DataLoader —https://github.com/entria/graphql-dataloader-boilerplate

As our source code base grows we need more and more tests to make sure we do not break things while we adding new features and doing some refactor. So how can I test a GraphQL server that uses mongoose under the hood?

I've looked through a look of testing frameworks and I realize that Jest is the best option available right now, its mocking system is a great idea and they also have a really cool feature called snapshot testing.

To start let's add jest to our project devDependencies:

yarn add jest jest-cli --dev

You should also add this to your package.json:

"jest": {
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/node_modules/",
"./dist"
],
"coverageReporters": [
"lcov",
"html"
],
"moduleNameMapper": {
"^mongoose$": "<rootDir>/node_modules/mongoose"
},
},

So, let's go to the important part, the GraphQL testing part.

I would like to show how to test UserType:

export default new GraphQLObjectType({
name: 'User',
description: 'Object with data related and only available to the logged user',
fields: () => ({
id: globalIdField('User'),
_id: {
type: GraphQLString,
resolve: user => user._id,
},
name: {
type: GraphQLString,
resolve: user => user.name,
},
email: {
type: GraphQLString,
resolve: user => user.email,
},
active: {
type: GraphQLBoolean,
resolve: user => user.active,
},
}),
interfaces: () => [NodeInterface],
});

The me field of ViewerType will be null when there is no user logged in, and it will return the current user when the token is a valid one.

The test is simple as follows:

import { graphql } from 'graphql';
import { schema } from '../../schema';
import {
User,
} from '../../models';
import {
getContext,
setupTest,
} from '../../../test/helper';

beforeEach(async () => await setupTest());
it('should be null when user is not logged in', async () => {
//language=GraphQL
const query = `
query Q {
viewer {
me {
name
}
}
}
`;

const rootValue = {};
const context = getContext();

const result = await graphql(schema, query, rootValue, context);
const { data } = result;

expect(data.viewer.me).toBe(null);
});

it('should return the current user when user is logged in', async () => {
const user = new User({
name: 'user',
email: 'user@example.com',
});
await user.save();
  //language=GraphQL
const query = `
query Q {
viewer {
me {
name
}
}
}
`;

const rootValue = {};
const context = getContext({ user });

const result = await graphql(schema, query, rootValue, context);
const { data } = result;

expect(data.viewer.me.name).toBe(user.name);
});

Edit — following the ideas of https://twitter.com/calebmer:

You can also use snapshot testing in this case:

expect(data.viewer.me).toBe(null); => expect(data).toMatchSnapshot();
expect(data.viewer.me.name).toBe(user.name); => expect(data).toMatchSnapshot();

The first test will test if UserType is null when user from the context is null.

The second test will test if me field of ViewerType is the same as the user logged in (passed in context object)

The trick is to connect to mongoose database before the first test, and clear the database for each test, like this:

import mongoose from 'mongoose';

import * as loaders from '../src/loader';

const { ObjectId } = mongoose.Types;

process.env.NODE_ENV = 'test';

const config = {
db: {
test: 'mongodb://localhost/test',
},
connection: null,
};

function connect() {
return new Promise((resolve, reject) => {
if (config.connection) {
return resolve();
}

const mongoUri = 'mongodb://localhost/test';

mongoose.Promise = Promise;

const options = {
server: {
auto_reconnect: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 1000,
},
};

mongoose.connect(mongoUri, options);

config.connection = mongoose.connection;

config.connection
.once('open', resolve)
.on('error', (e) => {
if (e.message.code === 'ETIMEDOUT') {
console.log(e);

mongoose.connect(mongoUri, options);
}

console.log(e);
reject(e);
});
});
}

function clearDatabase() {
return new Promise(resolve => {
let cont = 0;
let max = Object.keys(mongoose.connection.collections).length;
for (const i in mongoose.connection.collections) {
mongoose.connection.collections[i].remove(function() {
cont++;
if(cont >= max) {
resolve();
}
});
}
});
}

export async function setupTest() {
await connect();
await clearDatabase();
}

You can to add beforeEach in each test file as Jest does not have a global option for this.

We also have a helper to create our GraphQL context:

export function getContext(context) {
const dataloaders = Object.keys(loaders).reduce((dataloaders, loaderKey) => ({
...dataloaders,
[loaderKey]: loaders[loaderKey].getLoader(),
}), {});

return {
...context,
req: {},
dataloaders,
};
}

it will make sure all our tests have the dataloaders needed.

You can follow the same pattern to test Mutations, Connections, Interfaces, Subscriptions, and resolvers.

Tip: use //language=GraphQL to highlight graphql syntax

The result of this is here: https://github.com/sibelius/graphql-dataloader-boilerplate/pull/2

Feel free to reach me out on twitter to ask any questions: https://twitter.com/sseraphini

Or just open an issue to improve this boilerplate

We now run all our tests in parallel, this reduced the time of our takes greatly. Check this medium about the new architecture https://itnext.io/parallel-testing-a-graphql-server-with-jest-44e206f3e7d2