10

I'm trying to test a data source in my Apollo Server that based on Apollo Server's RESTDataSource (https://www.apollographql.com/docs/apollo-server/data/data-sources/#rest-data-source). I'm trying to test it using Jest. The class has methods that pull in data from an external REST API, as well as from another module that calls a second API (so this RESTDataSource ultimately depends on two external APIs, one of which is called directly here, and one of which is called indirectly).

I'm not an expert on testing, and I'm unclear how to mock the external APIs. GraphQL Tools has some tools that allow you to mock your server, but I'm not sure that's what I want. Or should I use Jest's methods for mocking ES6 classes, forgetting that this is a GraphQL server? If so, since I'm working with a class, do I just mock the methods using something like MyClass.myMethod as the mocked method?

Does anything change in how I do this if I'm using TypeScript (which I am), other than setting up Jest to work with TypeScript?

Obviously the correct route is to pick one of the options above, but I'm a bit 'not seeing the forest for the trees', that is, due to my inexperience with testing, I don't know which of these is the correct route to follow.

Thanks for any clues.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Cerulean
  • 5,543
  • 9
  • 59
  • 111

3 Answers3

5

Unit testing

You can unit test your data source by mocking the RESTDataSource in apollo-datasource-rest as suggested in apollo-datasource-rest + Typescript + Jest in the Apollo Spectrum chat.

For this data source:

import { RESTDataSource } from 'apollo-datasource-rest'

export class MyRestDataSource extends RESTDataSource {
  async getStackoverflow(): Promise<string> {
    return this.get('https://stackoverflow.com/')
  }
}

You could write an unit test like this:

import { MyRestDataSource } from './MyRestDataSource'

const mockGet = jest.fn()
jest.mock('apollo-datasource-rest', () => {
  class MockRESTDataSource {
    baseUrl = ''
    get = mockGet
  }
  return {
    RESTDataSource: MockRESTDataSource,
  }
})

describe('MyRestDataSource', () => {
  it('getStackoverflow gets data from correct URL', async () => {
    const datasource = new MyRestDataSource()

    await datasource.getStackoverflow()

    expect(mockGet).toBeCalledWith('https://stackoverflow.com/')
  })
})

Integration testing

Rather than unit testing the data sources, I'd in most cases prefer integration testing with e.g. apollo-server-testing: you run GraphQL against the server and test the entire path from the resolver to the data source. If you do so, consider using e.g. nock to mock the HTTP requests the data sources make.

TypeScript

The general approaches should be the same regardless of whether you're using TypeScript or JavaScript with just some minor differences. E.g. with JavaScript, your unit test could directly replace the get in the data source:

const MyRestDataSource = require('./MyRestDataSource')

describe('MyRestDataSource', () => {
  it('getStackoverflow gets data from correct URL', async () => {
    const datasource = new MyRestDataSource()
    datasource.get = jest.fn()
    await datasource.getStackoverflow()

    expect(datasource.get).toBeCalledWith('https://stackoverflow.com/')
  })
})

but with TypeScript that would cause a compiler error as get is protected:

MyRestDataSource.test.ts:6:16 - error TS2445: Property 'get' is protected and only accessible within class 'RESTDataSource' and its subclasses.

Lauri Harpf
  • 1,448
  • 1
  • 12
  • 30
0

If you're just looking to mock the underlying REST verbs, e.g. .get() and don't want to mock the entire RESTDataSource, you can do it this way:

jest.spyOn(RESTDataSource.prototype as any, 'get').mockImplementation(async () => {
  return Promise.resolve({ docs: 'hello' })
})
LaserWolf
  • 33
  • 1
  • 3
-1

Spoiler Alert: the below is relating to integration testing and is not in TypeScript but I thought it might help OP or others looking to test thoroughly their data sources.

Now for the answer: You can get inspiration from Apollo's excellent full stack tutorial repo . It helped me a lot. Here is a sample where you can see they mocked the response from the launchAPI and userAPI data sources.

    it('books trips', async () => {
    const {server, launchAPI, userAPI} = constructTestServer({
      context: () => ({user: {id: 1, email: 'a@a.a'}}),
    });

    // mock the underlying fetches
    launchAPI.get = jest.fn();

    // look up the launches from the launch API
    launchAPI.get
      .mockReturnValueOnce([mockLaunchResponse])
      .mockReturnValueOnce([{...mockLaunchResponse, flight_number: 2}]);

    // book the trip in the store
    userAPI.store = mockStore;
    userAPI.store.trips.findOrCreate
      .mockReturnValueOnce([{get: () => ({launchId: 1})}])
      .mockReturnValueOnce([{get: () => ({launchId: 2})}]);

    // check if user is booked
    userAPI.store.trips.findAll.mockReturnValue([{}]);

    const res = await server.executeOperation({
      query: BOOK_TRIPS,
      variables: {launchIds: ['1', '2']},
    });
    expect(res).toMatchSnapshot();
  });

And here is their constructTestServer function.

const constructTestServer = ({ context = defaultContext } = {}) => {
  const userAPI = new UserAPI({ store });
  const launchAPI = new LaunchAPI();

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    dataSources: () => ({ userAPI, launchAPI }),
    context,
  });

  return { server, userAPI, launchAPI };
};

module.exports.constructTestServer = constructTestServer;

Here is a more simple example I setup for a plain get request on the data source I called randomUserApi.

 it('randomUser > should return expected values', async () => {
    const randomUserApi = new RandomUserApi();

    const server = new ApolloServer({
      schema,
      dataSources: () => ({ randomUserApi }),
    });

    const mockResponse = {
      results: [
        {
          email: 'jordi.ferrer@example.com',
        },
      ],
      info: {
        seed: '54a56fbcbaf2d311',
      },
    };

    randomUserApi.get = jest.fn();
    randomUserApi.get.mockReturnValueOnce(mockResponse);

    const query = `query Query {
        randomUser {
          results {
            email
          }
          info {
            seed
          }
        }
      }`;

    // run query against the server and snapshot the output
    const response = await server.executeOperation({
      query,
    });

    const { data, errors } = response;

    expect(errors).toBeUndefined();
    expect(data).toEqual({
      randomUser: {
        info: { seed: '54a56fbcbaf2d311' },
        results: [{ email: 'jordi.ferrer@example.com' }],
      },
    });
  });

This is the code for RandomUserApi:

const { RESTDataSource } = require('apollo-datasource-rest');

class RandomUserApi extends RESTDataSource {
  constructor() {
    // Always call super()
    super();
    // Sets the base URL for the REST API
    this.baseURL = 'https://randomuser.me/';
  }

  async getUser() {
    // Sends a GET request to the specified endpoint
    return this.get('api/');
  }
}

module.exports = RandomUserApi;

And the resolver using it

  Query: {
    randomUser: async (_parent, _args, context) => context.dataSources.randomUserApi.getUser(),
  }

Full disclosure: same response posted here

Raphael
  • 9
  • 2