Blogs from the Ranch

< Back to Our Blog

Testing Your GraphQL Server

Avatar

Eric Walker

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 getBooksgetAuthors 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.

  • Schema testing
  • Query Testing
  • Mutation Testing
  • Resolver Testing

Now, let’s see them in action.

Schema Testing

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.

Query Testing

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)
})

Mutation Testing

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))
    })
  })
})

Resolver Testing

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?

Avatar

Eric Walker

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project