apollo-server: apollo-server-testing context is not receiving the `req` object

I am trying to write tests for header handling. I’m setting up the tests as follows:

it('should handle Accept-Language headers', async () => {
    const acceptLanguage = 'sv, en';

    const request = nock(config.API_URL)
      .matchHeader('accept-language', acceptLanguage)
      .get('/articles')
      .reply(200, []);

    const server = createServer();

    const { query } = createTestClient(server);

    await query({
      query: '{ articles { id } }',
      http: {
        headers: { 'accept-language': acceptLanguage },
      },
    });

    expect(request.isDone()).to.eql(true);
  });

The test relies on the req object being passed to the context function in the server setup, but I’m only getting an empty object. Is this the expected behavior? If so, how do you go about writing tests for authentication headers etc?

Thanks!

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 68
  • Comments: 39 (2 by maintainers)

Commits related to this issue

Most upvoted comments

I just ran into this.

I don’t consider it a true integration test unless you can actually test how your server’s true context is constructed. To hide behind a mock context doesn’t actually test anything.

I tried some of the solutions listed here and none of them worked for me. I ended up creating a new package that mimics the apollo-server-testing API, but allows for passing in a mock req object (or sets one for you automatically, with sensible defaults):

https://github.com/zapier/apollo-server-integration-testing

We’ve been using it successfully at my company for the last 6 months to write real integration tests. Posting it here in case anyone else is interested in giving it a try 😃

I’ve been facing a similar issue.

I’ve implemented a token-based authentication. The server checks that a valid token is sent in a header and then it passes the token in the context to the resolvers. When i’m making tests there is no sense in setting a test-token and mocking the token-validating function because that is what I want to test. It would be very useful to have tests that ensure that this part of the code is working correctly, doing something like this:

const { query: _query, mutate } = createTestClient(
  new ApolloServer({
    schema,
    context: ({ req }) => {
      const token = req.headers.token || '';
      return validateToken(token).then(validated => ({ tokenValidated: validated, token }));
    }
  })
);

Experiencing this and it’s making it impossible to write proper integration tests for logged in users etc.,

Edit: well, from the examples it seems like you are supposed to basically return a fixture from the server instance context, but I don’t think this is ideal.

I want to be able to test how my app integrates with apollo server, how can this be done properly if the behaviour of the apollo server when writing tests is different to what it will be like in production?

Hi, is there a proper solution atm? It is crazy that there is no supported way to pass custom headers to the req.

Are there any plans to solve this as part of the official testing package?

As others have already pointed out, not being able to mock out the req parameter on the context means that you cannot currently use apollo-server-testing to do true integration testing.

here are two solutions i came up with:

if you are comfortable “polluting” the prototype the prototype will only be modified when using this testServer. will not impact your actual server.

const { ApolloServer } = require("apollo-server-X"); // import from your flavor
const serverConfig = require("../api/config"); 

// server config is the object you pass into the ApolloServer constructor
// { resolvers, typeDefs, schemaDirectives, context, ... }

// execute the context function to get the base context object
// optionally you can add a default req or res in this step
const baseContext = serverConfig.context({});

// use 1 or more of the following functions as needed

ApolloServer.prototype.setContext = function setContext(newContext) {
  this.context = newContext;
}

ApolloServer.prototype.mergeContext = function mergeContext(partialContext) {
  this.context = Object.assign({}, this.context, partialContext);
}

ApolloServer.prototype.resetContext = function resetContext() {
  this.context = baseContext;
}

module.exports = {
  testServer: new ApolloServer({
    ...serverConfig,
    context: baseContext,
  }),
  baseContext, // easy access in tests
}

“cleaner” solution with a subclass

const { ApolloServer } = require("apollo-server-X"); // import from your flavor
const serverConfig = require("../api/config"); 

// server config is the object you pass into the ApolloServer constructor
// { resolvers, typeDefs, schemaDirectives, context, ... }

// execute the context function to get the base context object
// optionally you can add a default req or res in this step
const baseContext = serverConfig.context({});

// create a test server subclass with the methods built in
class ApolloTestServer extends ApolloServer {
  constructor(config) {
    super(config);
    this.context = baseContext;
  }

  setContext(newContext) {
    this.context = newContext;
  }

  mergeContext(partialContext) {
    this.context = Object.assign({}, this.context, partialContext);
  }

  resetContext() {
    this.context = baseContext;
  }
}

module.exports = {
  baseContext,
  testServer: new ApolloTestServer(serverConfig),
};

usage for either approach

const { createTestClient } = require("apollo-server-testing");
const { testServer, baseContext } = require("./test-utils/test-server");

const { query, mutate } = createTestClient(testServer);

test("something", async () => {
  // set / reset / merge the context as needed before calling query or mutate
  testServer.mergeContext({
    req: { headers: { Authorization: `Bearer ${token}` } },
  });

  const res = await query({ query, variables });
  expect(res)...
});

in typescript, context is private 🤕

here are two solutions i came up with:

if you are comfortable “polluting” the prototype the prototype will only be modified when using this testServer. will not impact your actual server.

const { ApolloServer } = require("apollo-server-X"); // import from your flavor
const serverConfig = require("../api/config"); 

// server config is the object you pass into the ApolloServer constructor
// { resolvers, typeDefs, schemaDirectives, context, ... }

// execute the context function to get the base context object
// optionally you can add a default req or res in this step
const baseContext = serverConfig.context({});

// use 1 or more of the following functions as needed

ApolloServer.prototype.setContext = function setContext(newContext) {
  this.context = newContext;
}

ApolloServer.prototype.mergeContext = function mergeContext(partialContext) {
  this.context = Object.assign({}, this.context, partialContext);
}

ApolloServer.prototype.resetContext = function resetContext() {
  this.context = baseContext;
}

module.exports = {
  testServer: new ApolloServer({
    ...serverConfig,
    context: baseContext,
  }),
  baseContext, // easy access in tests
}

“cleaner” solution with a subclass

const { ApolloServer } = require("apollo-server-X"); // import from your flavor
const serverConfig = require("../api/config"); 

// server config is the object you pass into the ApolloServer constructor
// { resolvers, typeDefs, schemaDirectives, context, ... }

// execute the context function to get the base context object
// optionally you can add a default req or res in this step
const baseContext = serverConfig.context({});

// create a test server subclass with the methods built in
class ApolloTestServer extends ApolloServer {
  constructor(config) {
    super(config);
    this.context = baseContext;
  }

  setContext(newContext) {
    this.context = newContext;
  }

  mergeContext(partialContext) {
    this.context = Object.assign({}, this.context, partialContext);
  }

  resetContext() {
    this.context = baseContext;
  }
}

module.exports = {
  baseContext,
  testServer: new ApolloTestServer(serverConfig),
};

usage for either approach

const { createTestClient } = require("apollo-server-testing");
const { testServer, baseContext } = require("./test-utils/test-server");

const { query, mutate } = createTestClient(testServer);

test("something", async () => {
  // set / reset / merge the context as needed before calling query or mutate
  testServer.mergeContext({
    req: { headers: { Authorization: `Bearer ${token}` } },
  });

  const res = await query({ query, variables });
  expect(res)...
});

apollo-server-testing is a pretty thin wrapper around executeOperation, which is basically a method that lets you execute a GraphQL operation without going through the HTTP layer.

We’re currently figuring out what parts of Apollo Server are really essential and which could be pared down in the upcoming AS3. My instincts are that apollo-server-testing doesn’t provide a huge amount of standalone value, and that we should document the direct use of executeOperation for tests that don’t want to test things that require the HTTP layer (like headers and cookies), and that we should encourage folks to use https://github.com/zapier/apollo-server-integration-testing for tests that require the HTTP layer (at least if you’re using Express!). I’m tracking this in #4952.

I don’t think it’s likely that we’ll change the existing apollo-server-testing to work differently during v2 (ie, we’re probably not going to add anything Express-specific to it), so I’m going to close this issue. It’s great to see that there are so many approaches to doing these tests, including apollo-server-integration-testing!

I found a much simpler solution to set the initial context arguments. What it does is that it wraps the context function with another function that “injects” the context argument:

const { ApolloServer } = require('apollo-server') // Or `apollo-server-express`
const { createTestClient } = require('apollo-server-testing')

/**
 * Simple test client with custom context argument
 * @param config Apollo Server config object
 * @param ctxArg Argument object to be passed
 */
const testClient = (config, ctxArg) => {
  return createTestClient(new ApolloServer({
    ...config,
    context: () => config.context(ctxArg)
  }))
}

Usage:

const { query, mutate } = testClient(config, { req: { headers: { authorization: '<token>' } } })

// Use as usual
query({
  query: GET_USER,
  variables: { id: 1 }
})

If you need to set custom context arguments per query or mutate, it can be further extended to be something like this:

const { ApolloServer } = require('apollo-server') // Or `apollo-server-express`
const { createTestClient } = require('apollo-server-testing')

/**
 * Test client with custom context argument that can be set per query or mutate call
 * @param config Apollo Server config object
 * @param ctxArg Default argument object to be passed
 */
const testClient = (config, ctxArg) => {
  const baseCtxArg = ctxArg
  let currentCtxArg = baseCtxArg

  const { query, mutate, ...others } = createTestClient(new ApolloServer({
    ...config,
    context: () => config.context(currentCtxArg)
  }))

  // Wraps query and mutate function to set context arguments
  const wrap = fn => ({ ctxArg, ...args }) => {
    currentCtxArg = ctxArg != null ? ctxArg : baseCtxArg
    return fn(args)
  }

  return { query: wrap(query), mutate: wrap(mutate), ...others }
}

Usage:

const { query, mutate } = testClient(config, { req: { headers: { authorization: '<token>' } } })

// Set context argument per query or mutate
query({
  query: GET_USER,
  variables: { id: 1 },
  ctxArg: { req: { headers: { authorization: '<new-token>' } } }
})

Hope this helps 😃

I know there are many solutions here already, but for some reason they weren’t sitting well with me. I made a very thin wrapper around createTestClient that does perfectly for me. Maybe it’ll help someone else, too 😃

import { createTestClient } from 'apollo-server-testing'
import { ApolloServer } from 'apollo-server'

// This is a simple wrapper around apollo-server-testing's createTestClient.
// A massive shortcoming of that code is that the ctx object gets passed in
// to the context function as an empty object. So, critically, we can't test
// authorization headers, which is gonna be like all the requests we do in our
// tests. This wrapper allows some headers to be passed in. See:
// https://github.com/apollographql/apollo-server/issues/2277
export default function (server: ApolloServer, headers = {} as any) {
  // @ts-ignore B/c context is marked as private.
  const oldContext = server.context

  const context = ({ req, res }) => {
    return oldContext({ res, req: { ...req, headers }})
  }

  const serverWithHeaderContext = Object.assign({}, server, { context })
  // @ts-ignore -- Typescript doesn't know about __proto__, huh...
  serverWithHeaderContext.__proto__ = server.__proto__

  return createTestClient(serverWithHeaderContext)
}

And instead of import { createTestClient } from 'apollo-server-testing', just do a import createTestClient from 'path/to/this/module'. Enjoy!

@liyikun 😢 well that sucks lol

Sent with GitHawk

Thanks for the heads up @dipiash, I faced this last week myself, I’ve updated the gist as well

Amalgamated the work by @vitorbal, the Zapier Team Apollo-Server-Integration-Testing, and the Apollo Server Testing Library

Here’s some of the changes that have been made

  1. Allows to pass headers while making an operation, this is in contrast to how headers are set in Apollo-Server-Integration-Testing
  2. Changed the operation response type to GraphQLResponse to give more type information while testing

https://gist.github.com/preetjdp/178643c5854ae775b005834be6687edc

Hello, @preetjdp!

Last few days I try to update dependencies for apollo-server. I have some problems with integration tests. And I found your solution and try.

I think that line 142 is incorrect https://gist.github.com/preetjdp/178643c5854ae775b005834be6687edc#file-createtestclient-ts-L142

...args.variables,

If I start test with query:

const res = await query({
   query: SOME_QUERY,
   variables: { ids: [-3, -2] },
   headers: {
      authorization: undefined,
   },
})

I have the incorrect error:

HttpQueryError: {"errors":[{"message":"Variable \"$ids\" of required type \"[Int]!\" was not provided.","locations":[{"line":1,"column":18}],"extensions":{"code":"INTERNAL_SERVER_ERROR"}}]}

But if I change line 142 on (I’m forked your gist https://gist.github.com/dipiash/bf69150518baf8ddb9b0e136fdf3c9d0#file-createtestclient-ts-L142):

variables: args.variables,

I have correct result.

The following code worked for me.

import { ApolloServer } from "apollo-server"
import { createTestClient } from "apollo-server-testing"

const createTestServer = () => {
  const server = new ApolloServer({ 
    // ...
  });
  return server;
};

test('auth', async () => {
  const server = createTestServer();
  const { mutate } = createTestClient(server);

  server.context = () => {
    const req = {
      headers: {
        "authorization": `accessToken ${ accessToken }`
      }
    }
    return { req }
  };

  response = await mutate({
    mutation: MUTATION_1,
    variables: { var1, var2 }
  });
}

@KristianWEB this has also helped us with our ApolloServer testing. Thank you!

@FrancescoSaverioZuppichini I’ve implemented a custom testClient. Check it out

Unfortunately, apollo-server-testing doesn’t work when you need to work with req and res objects in the server side. Also none of the solutions really worked out for me because req and res are still mocks created manually by developers. As a result, when you do something like res.cookie(...) in your express server side, you’ll have an error because the mock res object doesn’t have something like cookie function.

I had to resort to using a graphql client created from apollo-boost and making real graphql requests to do integration testing.

I found a solution through supertest

// create-app.ts
import { ApolloServer } from 'apollo-server-express'
import { config as configEnv } from 'dotenv'
import express from 'express'
import 'reflect-metadata'
import { createSchema } from './create-shema'
import { getContext } from './get-context'

configEnv()

export async function createApp() {
  const server = new ApolloServer({
    schema: await createSchema(),
    context: getContext,
  })

  const app = express()

  server.applyMiddleware({ app })

  return { server, app }
}
// query test
  test('get auth user', async () => {
    const { app } = await createApp()
    const [userData] = fakeUsers

    const user = await usersService.findUser({ email: userData.email })
    const token = authService.createToken(user!.id)

    const meQuery = `
      {
        me {
          id
          name
          email
          passwordHash
        }
      }
    `

    const result = await makeQuery({ app, query: meQuery, token })

    expect(result.errors).toBeUndefined()
    expect(result.data).toBeDefined()

    expect(result.data).toHaveProperty('me', {
      id: user!.id.toString(),
      name: user!.name,
      email: user!.email,
      passwordHash: expect.any(String),
    })
  })
// make-query
import { Express } from 'express'
import supertest from 'supertest'

type MakeQuery = {
  app: Express
  query: string
  token?: string
  variables?: object
}

export async function makeQuery({ token, query, app, variables }: MakeQuery) {
  const headers: { Authorization?: string } = {}

  if (token) {
    headers.Authorization = `Bearer ${token}`
  }

  const { body } = await supertest(app)
    .post('/graphql')
    .send({ query, variables })
    .set(headers)

  return body
}

Apologies for recommending other package, but it is from the same ecosystem, not the competing one. I hope it is not displeasing the authors.

Since I could not find any pleasant solution, I have ended up writing tests using apollo-boost:

import ApolloClient, { gql } from "apollo-boost"
import fetch from "node-fetch"

test("graphql response with auth header", async () => {
    const uri = "http://localhost:4000/graphql"
    const client = new ApolloClient({
        uri,
        fetch,
        request: operation => {
            operation.setContext({
                headers: {
                    authorization: "Bearer <token>",
                },
            })
        },
    })

    const queryResponse = await client.query({ query: gql`query{ ... } ` })
    const mutationResponse = await client.mutate({ mutation: gql`mutation{ ... }` })

    expect(queryResponse.data).toBe("expected-qeury-data")
    expect(mutationResponse.data).toBe("expected-mutation-data")
})

I am not really sure if this is still called “integration testing” and not “e2e” but it works really for me. Thought this might come handy to someone still struggling.

Here is the gist of the approach I mentioned yesterday. This is a module that creates and exports a singleton test client. This assumes you’re using Jest, and that your context factory is in its own module. I think the paths are self explanatory but feel free to ask if anything isn’t clear.

You could probably modify this to be a test client builder rather than a singleton which would be a little safer so you don’t have to worry about maintaining token state between tests.

let token;

/* IMPORTANT
 * This uses `doMock` instead of `mock` to prevent hoisting and allow the use of
 * a local variable (specifically, `token`). This `doMock` call MUST be before
 * the require() call to ../graphql/graphqlServer, as otherwise that module will
 * not use the mocked context factory.
 */
jest.doMock('../graphql/context.js', () => {
  const contextFactory = jest.requireActual('../graphql/context.js');
  return jest.fn(() =>
    contextFactory({
      req: { headers: token ? { authorization: `Bearer ${token}` } : {} }
    })
  );
});

const { createTestClient } = require('apollo-server-testing');
const createServer = require('../graphql/graphqlServer');

// see https://www.apollographql.com/docs/apollo-server/testing/testing/
const testGraphqlClient = createTestClient(createServer());

testGraphqlClient.setToken = function(newToken) {
  token = newToken;
};

module.exports = testGraphqlClient;

In case the context factory isn’t clear, the idea is that your ApolloServer is instantiated like so:

new ApolloServer({
  ...otherStuff
  context: require('./context.js')
})

… and the context.js file is like:

module.exports = async ({ req }) => {
  return {
    ...allYourFancyContextStuff
  };
};

Hope this helps someone.