loading...
Cover image for Testing Apollo Server with Typescript

Testing Apollo Server with Typescript

learnitmyway profile image David Updated on ・4 min read

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

Note: The most up to date version of this article is on my website.

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 }: { dataSources: any }
    ): 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/MoviesAPI.test.ts

import { Body } from 'apollo-datasource-rest/dist/RESTDataSource'
import gql from 'graphql-tag'

import MoviesAPI from './MoviesAPI'
import { Movie } from './types'

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

class MoviesAPIFake extends MoviesAPI {
  async get(path: string): Promise<any> {
    return super.get(path)
  }
}

describe('MoviesAPI', () => {
  it('fetches all movies', async () => {
    // We cannot stub a protected method,
    // so we create a fake.
    const moviesAPI = new MoviesAPIFake()

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


Before you go… Thank you for reading this far! If you enjoyed the article, please don't forget to ❤️ it.

I write about my professional and educational experiences as a self-taught software developer, so click the +FOLLOW button if this interests you! You can also check out my website or subscribe to my newsletter for more content.

You might also like:

Posted on by:

learnitmyway profile

David

@learnitmyway

I write about my professional and educational experiences as a self-taught software developer.

Discussion

markdown guide
 

I've recently added tests and TypeScript to an ApolloServer project, so I got curious how our implementations differ. I made a PR to your repo with types in the resolvers (which I found to be the least obvious part in terms of how to add typing). Hope it helps, but feel free to ignore it 😀

 

Thanks, that was really helpful!