Testing Apollo Server with Typescript

In this article, I will demonstrate a way to test GraphQL endpoints of an Apollo Server with a RESTDataSource in Typescript.

scaffolding
Photo by Yves Alarie on Unsplash

Background

At the beginning of the month, I joined a new project that is using Apollo Server as a back end for a front end. Some months before I joined, a RESTDataSource was introduced but the implemented code didn’t get tested. I was lucky enough to join the team in the middle of writing those missing tests and this article is a demonstration of what I came up with.

Disclaimer: I only had minimal experience with GraphQL beforehand and I still haven’t invested much time into Typescript. However, my knowledge of testing should be pretty sound! That aside, having this article a week ago might have saved me a day’s work.

The following exercise is inspired by the docs and also uses apollo-server-testing.

Exercise

The app is a simple GraphQL server that can be consumed as follows:

  query GetMovies {
    movies {
      id
      title
    }
  }
  mutation CreateMovie($newMovie: NewMovie!) {
    createMovie(newMovie: $newMovie) {
      movies {
        id
        title
      }
    }
  }

Feel free to follow along with the source code. (If you need help setting up eslint or nodemon, it might also be worth having a look.) Let me show you how I tested the query.

The code under test

Let’s start with the MoviesAPI:

// src/MoviesAPI.ts

import { RESTDataSource } from 'apollo-datasource-rest'
import { Movie } from './types'

export default class MoviesAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = 'http://localhost:5200/'
  }

  async getMovies(): Promise<Movie[]> {
    return this.get('movies')
  }
}

As you can see, we have a method that fetches movies from a REST API being served at http://localhost:5200/ with a movies endpoint.

The data sources:

// src/dataSources.ts

import MoviesAPI from './MoviesAPI'

const dataSources = (): any => {
  return {
    moviesAPI: new MoviesAPI(),
  }
}

export default dataSources


The resolvers:

// src/resolvers.ts

import { Movie } from './types'

const resolvers = {
  Query: {
    movies: (
      _: void,
      __: void,
      { dataSources }: Context
    ): Promise<Movie[]> =>
      dataSources.moviesAPI.getMovies(),
  },
}

export default resolvers

The type definitions:

// src/typeDefs.ts

import { gql } from 'apollo-server'

const typeDefs = gql`
  type Movie {
    id: String
    title: String
  }

  type Query {
    movies: [Movie]
    movie(id: ID!): Movie
  }
`

export default typeDefs

The test

To test our code we can use apollo-server-testing and create the test server as follows:

// src/testUtils/testServer.ts

import {
  createTestClient,
  ApolloServerTestClient,
} from 'apollo-server-testing'
import { ApolloServer } from 'apollo-server'
import resolvers from '../resolvers'
import typeDefs from '../typeDefs'

export default function testServer(
  dataSources: any
): ApolloServerTestClient {
  return createTestClient(
    new ApolloServer({ typeDefs, resolvers, dataSources })
  )
}

Then we can create our first test as follows:

// src/resolvers.test.ts

import gql from 'graphql-tag'

import { Movie } from './types'

import testServer from './testUtils/testServer'
import { moviesSample } from './testUtils/moviesSample'
import MoviesAPI from './MoviesAPI'

describe('MoviesAPI', () => {
  it('fetches all movies', async () => {
    // We cannot stub a protected method,
    // so we declare the type as 'any'
    const moviesAPI: any = new MoviesAPI()

    // We create a stub because we don't
    // want to call an external service.
    // We also want to use it for testing.
    const getStub = (): Promise<Movie[]> =>
      Promise.resolve(moviesSample())
    moviesAPI.get = jest.fn(getStub)

    // We use a test server instead of the actual one.
    const { query } = testServer(() => ({ moviesAPI }))

    const GET_MOVIES = gql`
      query GetMovies {
        movies {
          id
          title
        }
      }
    `

    // A query is made as if it was a real service.
    const res = await query({ query: GET_MOVIES })

    // We ensure that the errors are undefined.
    // This helps us to see what goes wrong.
    expect(res.errors).toBe(undefined)

    // We check to see if the `movies`
    // endpoint is called properly.
    expect(moviesAPI.get).toHaveBeenCalledWith('movies')

    // We check to see if we have
    // all the movies in the sample.
    expect(res?.data?.movies).toEqual(moviesSample())
  })
})

Ways to break the test

The test would break if:

  • someone accidentally deletes anything from our resolver, data source method or associated type definitions
  • someone adds a required field to the associated type definitions
  • someone accidentally renames the endpoint
  • GraphQL throws an error
  • (Anything else?)

Caveats

If you have a lot of additional logic in your data sources or resolvers, I could imagine that it might be difficult to locate the source of an error thrown. In this case, it might make more sense, to add some unit tests alongside the integration test shown above.

Final Thoughts

For the sake of demonstration, I first showed the application code and then showed you how I would test it. In practice, I would recommend doing it the other way round.

I only ended up showing you how to test a query. If you are interested in how to test a mutation and see how the rest of the code was implemented, feel free to have a look at the repo.

As I said at the beginning, I am quite new to GraphQL and I haven’t invested much time into Typescript, so any feedback would be great. Getting rid of those anys would be especially helpful.


Timeline:

  • April 2020: First published
  • May 2020: Updated code examples

David

Thanks for visiting Learn it my way! I created this website so I could share my learning experiences as a self-taught software developer. Subscribe to for the latest content if this interests you!

Profile pic

David

Thanks for visiting Learn it my way! I created this website so I could share my learning experiences as a self-taught software developer. Subscribe to for the latest content if this interests you!