Testing Apollo Server With Vitest

Cover Image for Testing Apollo Server With Vitest
Peter Nycander
Peter Nycander

When it comes to testing Javascript or Typescript projects, Jest has been the test runner of choice for most teams for a long time. However, Jest has some issues. It is often a pain to configure, mock values, and it is quite slow when you add modern requirements like Typescript.

A few projects have been started to address this pain. The top ones that comes to mind is Rome and most recently Vitest. Being a fan of Vite I decided to test out Vitest for a pet project in Node.js and TypeScript using Apollo Server. I also believe in using something like Supertest when testing Node.js servers so that you can test what your users use (the http layer). It is the testing philosophy that guided the design of React Testing Library:

The more your tests resemble the way your software is used, the more confidence they can give you.

Enough background, this is how I added the setup to my project. Please note that these things tend to change and get out of date. Visit the docs for the different projects for correct instructions, but this is how I did it early January 2022.

Basic setup

Start with installing the packages

npm install -D vitest supertest apollo-server-express graphql @types/supertest

We install apollo-server-express even if I used some other web framework, just because it plays nicely with Supertest. We won't get as good mapping to how users use the server, but it is a tradeoff that I am willing to make.

Add the following to the scripts part of your package.json:

"test": "vitest run",
"test:coverage": "vitest --coverage",
"test:watch": "vitest watch",

Create a file in the root of your project called vitest.config.ts with the content

import { defineConfig } from "vite";

export default defineConfig({
  test: {
    testTimeout: 2000,
    setupFiles: [],
    coverage: {
      reporter: ["text", "html"],
    },
    global: true,
  },
});

Add the following to your tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/global"]
  }
}

Create a file testUtils.ts in a folder where you want your tests to live with the following content:

import { ApolloServer } from "apollo-server-express";
import express from "express";
import { DocumentNode, print } from "graphql";
import http from "http";
import request from "supertest";
import { apolloConfig } from "../server"; // A function to create the config for an Apollo Server. This is different from project to project, so take in your configuration from your project.

let cachedServer: any;

const createServer = async () => {
  const app = express();
  const server = new ApolloServer(apolloConfig());
  const httpServer = http.createServer(app);
  await server.start();
  server.applyMiddleware({ app });
  return httpServer;
};

export const sendTestRequest = async (
  query: DocumentNode,
  {
    variables = {},
    headers = {},
  }: {
    variables?: any;
    headers?: { [key: string]: string };
  } = {}
): Promise<any> => {
  const server = cachedServer ?? (await createServer());
  cachedServer = server;
  const requestBuilder = request(server).post("/graphql");

  Object.entries(headers).forEach(([key, value]) => {
    requestBuilder.set(key, value);
  });
  const { text } = await requestBuilder.send({
    variables,
    query: print(query),
  });
  return JSON.parse(text);
};

Now you can create your first test file!

Writing tests

Make sure you name end your test file names with .spec.ts or .test.ts to make sure vitest picks them up.

To make sure everything works correctly, create a file somewhere in your project named example.test.ts with the following content:

import { gql } from "apollo-server-express";
import { sendTestRequest } from "../testUtils"; // Make sure this points to the file we created earlier!

it("Example", async () => {
  const response = await sendTestRequest(gql`
    query {
      __typename
    }
  `);

  expect(response).toEqual({
    data: {
      __typename: "Query",
    },
  });
});

And run npm test.

If everything went well, you should see something like this as output:

RUN /home/coder/blog/backend

√ src/graphql/tests/example.test.ts (1)

Test Files 1 passed (1)
    Tests 1 passed (1)
     Time 1.66s (in thread 58ms, 2872.68%)

If you want to mock a value you can do it like this example:

import { gql } from "apollo-server-express";
import { sendTestRequest } from "../testUtils";
import { sign } from "utils/jwt";
import * as OrganizationModel from "models/Organization";

it("Example with mock", async () => {
  const spy = vi
    .spyOn(OrganizationModel, "get")
    .mockImplementation(async (id) => ({
      id,
    }));

  const response = await sendTestRequest(
    gql`
      query {
        currentOrganization {
          id
        }
      }
    `,
    {
      headers: {
        Authorization: `Bearer ${await sign({
          organizationId: "MockOrgId",
          userId: "MockUserId",
        })}`,
      },
    }
  );

  expect(spy).toHaveBeenCalledTimes(1);
  expect(response).toEqual({
    data: {
      currentOrganization: {
        id: "MockOrgId",
      },
    },
  });
});

Conclusion

It felt like I did something wrong. This was too easy when you are used to Jest.