Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
We’re continuing our GraphQL series with a look at how to best test your GraphQL server. If you’d like a bit of a refresher, check out our What is GraphQL? post.
GraphQL provides the ability to expose APIs in a way that is flexible and performant for clients. This flexibility can come at a cost in complexity for server implementations. While there are many options available in terms of technology stacks for implementing GraphQL, one of the most popular is Node.js and Javascript. Here is a link to an article discussing Testing on Node.js in general.
Below is a GraphQL server app built on Node.js, Apollo and Express.
import express from 'express' import { graphqlExpress, graphiqlExpress } from 'apollo-server-express' import { schema } from './schema.js' // Initialize the app const app = express() // The GraphQL endpoint app.use('/graphql', express.json(), graphqlExpress({ schema })) // GraphiQL, a visual editor for queries app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })) // Start the server app.listen(3000, () => { console.log('Go to http://localhost:3000/graphiql to run queries!') })
You will be working with the following GraphQL definition defined in schema.gql
.
type Book { title: String! author: String! } type Query { getBooks: [Book] getAuthors: [String] } input BookInput { title: String! author: String! } type Mutation { addBook(input: BookInput!): Book }
The executable schema loaded into graphqlExpress
needs two things: the file containing the GraphQL types and the resolvers for getBooks
, getAuthors
and addBook
.
import { resolvers } from './resolvers.js' import { makeExecutableSchema } from 'graphql-tools' import fs from 'fs' import path from 'path' export const typeDefs = fs.readFileSync(path.join(__dirname, '.', 'schema.gql'), 'utf8') // Put together a schema export const schema = makeExecutableSchema({ typeDefs, resolvers, })
Here is your incredibly sophisticated yet lightweight in-memory database of Books that will support the resolvers.
export const books = [ { title: "Harry Potter and the Sorcerer's stone", author: 'J.K. Rowling', }, { title: 'Jurassic Park', author: 'Michael Crichton', } ]
Finally, the resolvers themselves that will provide and mutate your book data.
import { books } from './models.js' export const resolvers = { Query: { getBooks: () => books, getAuthors: () => { return books.map (book => { return book.author }) } }, Mutation: { addBook: (_, args) => { const book = { title: args.input.title, author: args.input.author } books.push(book) return book } } }
There are four primary areas of interest with server-side GraphQL. I’ve demonstrated testing strategies for each part below.
Now, let’s see them in action.
Chai.js is an assertion library built for Node in general, rather than specifically for GraphQL. Use assertions to verify your schema was loaded correctly.
import { describe, it } from 'mocha' import chai from 'chai' import { schema } from '../schema.js' chai.should() describe('Test Static Schema Snapshot', () => { it('schema should contain types', () => { chai.assert.isNotNull(schema.getType("Book")) chai.assert.isDefined(schema.getType("Book")) }) it('scheme should not contain unregistered types', () => { chai.assert.isUndefined(schema.getType("NotADefinedType", "Type should not be defined")) }) })
These static tests could be written with any unit testing library, so feel free to substitute your favorite.
To start testing GraphQL queries, use the easygraphql-tester library. The library can be used to test all kinds of resolvers, queries, mutations, and subscriptions, but start by testing the getBooks
query.
First, create a tester object from your schema.
import { describe, it, before } from 'mocha' import { resolvers } from '../resolvers.js' import { books } from '../models.js' import EasyGraphQLTester from 'easygraphql-tester' import { typeDefs } from '../schema.js' describe('Test Queries', () => { let tester before(() => { tester = new EasyGraphQLTester(typeDefs, resolvers) }) // ...tests go here })
Next, create a test which calls the allBooks
query and verify that the call is successful (by passing in true
) and that it returns the correct book data (by providing the expected data).
it('Should pass if the query is valid', () => { const validQuery = ` { getBooks { title } } ` tester.test(true, validQuery, { books: books }) })
Finally, create a failing query by asking GraphQL for a property that doesn’t exist on Book
.
it('Should fail if the query is invalid', () => { const invalidQuery = ` { getBooks { # Not a field! publicationDate } } ` tester.test(false, invalidQuery) })
An important part of GraphQL is the ability to modify system state using the concept of a Mutation. A Mutation is a defined type that declares a mutation API with given inputs and expected outputs.
Using the following Mutation definition.
input BookInput { title: String! author: String! } type Mutation { addBook(input: BookInput!): Book }
Define a series of tests to validate your Mutation fails appropriately when missing the required input. Also, provide a test that verifies that your system is in the correct state after performing a mutating action. In this case by adding a Book to your in-memory Book database.
import { describe, it, before } from 'mocha' import { expect } from 'chai' import EasyGraphQLTester from 'easygraphql-tester' import fs from 'fs' import path from 'path' import { books } from '../models.js' import { resolvers } from '../resolvers.js' const schemaCode = fs.readFileSync(path.join(__dirname, '.', 'schema.gql'), 'utf8') describe('Test Mutation', () => { let tester before(() => { tester = new EasyGraphQLTester(schemaCode, resolvers) }) describe('Should throw an error if variables are missing', () => { it('Should throw an error if the variables are missing', () => { let error try { const mutation = ` mutation AddBook($input: BookInput!) { addBook(input: $input) { title author } } ` tester.mock(mutation) } catch (err) { error = err } expect(error).to.be.an.instanceOf(Error) expect(error.message).to.be.eq('Variable "$input" of required type "BookInput!" was not provided.') }) }) describe('Should add a book', () => { it('Should add Pet Cemetary to the list of books', () => { const mutation = ` mutation AddBook($input: BookInput!) { addBook(input: $input) { title author } } ` const bookCount = books.length tester.graphql(mutation, undefined, undefined, {input: { title: 'Pet Cemetary', author: 'Stephen King' }}).then(result => { expect(books.length).to.be.eq(bookCount+1) }) .catch(err => console.log(err)) }) }) })
Resolvers are pure functions (no side effects) that support a query’s ability to fetch and transform results based on their requirements. In this example, define a simple getAuthors
resolver that is implemented via a map transformation over your books database. Invoking the getAuthors
query will resolve to this function at runtime.
export const resolvers = { Query: { getBooks: () => books, // Access your real database of books getAuthors: () => { return books.map (book => { return book.author }) } } ... }
Testing this resolver is demonstrated below. You construct a query specification for the getAuthors
query and execute against schema graph. You can then inspect the results of the query via promise fulfillment and perform standard chai assertions on the state.
import { describe, it, before } from 'mocha' import { expect } from 'chai' import EasyGraphQLTester from 'easygraphql-tester' import { typeDefs } from '../schema.js' import { books } from '../models.js' import { resolvers } from '../resolvers.js' describe("Test Resolvers", () => { let tester; before(() => { tester = new EasyGraphQLTester(typeDefs, resolvers); }); it("should return expected values", async () => { const query = ` { getAuthors }` const args = {} const result = await tester.graphql(query, {}, {}, args) expect(result.data.getAuthors.length).to.be.eq(books.length) expect(result.data.getAuthors[0]).to.be.eq("J.K. Rowling") }); });
With the tools above you can effectively manage the complexities of GraphQL on the server. The ability to validate your schema definitions, queries, mutations, and resolvers are valuable components ensuring quality in your systems and confidence when you need to introduce changes to your code.
If you’d like to explore more of what GraphQL can offer, check out our posts on Building a GraphQL Server with Java and GraphQL versus REST. Also, if you aren’t sure if GraphQL is the best fit, check out Is GraphQL Right for My Project?
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...