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!
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?
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'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 }));
}
})
);
i am using workaround based on .createHandler - here is a gist
could not figure out why
server.executeOperation({http})does not work
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)...
});
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 🤕
@liyikun 😢 well that sucks lol
Sent with GitHawk
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 think you could put a spy around the context factory and inject a request object that way. I've been tinkering a bit with it and I'll post a gist if I get it to work.
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.
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.
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!
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
}
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've tried to implement your example but I'm getting the error: Cannot read property 'headers' of undefined
Check the code
@KristianWEB You're instantiating a different server than the one in src/index.js. You should use the latter instead since its context property passes the req value.
Also, you shouldn't be passing an ApolloServer instance to testClient. The first parameter actually accepts the apollo server config object, but since the instance had the context property too, it also worked.
@BjornLuG What should the apollo server config object be in this case? I couldn't find any information about it.
Edit: I see what you meant: adding the resolvers and the schema. Now the apollo server doesnt recognize the context ( config.context is not a function )
const { mutate, query } = testClient({
resolvers,
typeDefs,
});
When I try to add the context to the testClient config in this way:
const { mutate, query } = testClient({
resolvers,
typeDefs,
context: () => ({}),
});
It throws me the headers of undefined error again. I've tried this as well:
const { mutate, query } = testClient({
resolvers,
typeDefs,
context: async ({ req }) => ({ req }),
});
But with no success I'm still receiving Cannot destructure property 'req' of 'undefined' as it is undefined error.
How do I instantiate the testClient properly? Thanks again!
Code
@iensu how is it on your side? Did you solve the context problem?
@KristianWEB The testClient function doesn't check if the context is defined or not in the config object, so it throws the config.context is not a function error. You can add the extra checks if you want.
But the ideal way to setup the testClient is to first export the config object from src/index.js and then import it into the testClient function's first parameter. This way if you ever update the any other properties on your config object, testClient will use that as well, preventing multiple sources of truth.
From your third code snippet, it already matches the config of the one in src/index.js, which should work. The error Cannot destructure property 'req' of 'undefined' as it is undefined probably means that you didn't provide a context argument when calling the function.
You can add the context argument in either testClient's second parameter, or the ctxArg property when calling query or mutate. It must be defined in at least one of this two places.
@BjornLuG I was able to solve it but in kind of a weird way, consider the following code:
// use the test server to create a query function
const { mutate, query } = testClient(
{
resolvers,
typeDefs,
context: async ({ req }) => ({ req }),
},
{
req: { headers: { authorization: `JWT ${tokenA}` } },
}
);
// create a post from authenticated user ( user A )
const post = await mutate({
mutation: CREATE_POST,
variables: {
body: "example post",
},
});
// like a post from other user ( user B ) ?? HOW
const tokenB = generateToken(userB);
await mutate({
mutation: LIKE_POST,
variables: {
postId: post.data.createPost.id,
},
ctxArg: { req: { headers: { authorization: `JWT ${tokenB}` } } },
});
Using this example I had to provide the context in the testClient itself and in queries/mutations whenever I need to use other user ( in this case userB ). I couldn't make it work to use the context whenever I need but I guess it's still fine.
Now onto the 3rd example that I provided early as I said it throws the error Cannot destructure property 'req' of 'undefined' as it is undefined but I can see that I'm successfully creating a post:
// ACT
// use the test server to create a query function
const { mutate, query } = testClient({
resolvers,
typeDefs,
context: async ({ req }) => ({ req }), // TypeError: Cannot destructure property 'req' of 'undefined' as it is undefined.
});
// create a post from authenticated user ( user A )
const post = await mutate({
mutation: CREATE_POST,
variables: {
body: "example post",
},
ctxArg: { req: { headers: { authorization: `JWT ${tokenA}` } } },
});
console.log(post); // returns successfully the new created post
Let me know if I'm missing something 🤔🤔
have a look at the solution i posted originally. it works https://github.com/apollographql/apollo-server/issues/2277#issuecomment-515633359
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.
Found a solution to still test production code, but mock the req
// createApolloServer.js
// authorizationStore is anything that could add fields to req
export const createContext = (authorizationStore) => ({ req }) => {
authorizationStore.setProfileId(req); // of course in tests this won't work because req is null
return {
userId: req.profile.id
};
};
export const createApolloServer (authorizationStore, context = createContext) => {
const apolloServer = new ApolloServer({
schema,
context: context(authorizationStore)
});
return apolloServer;
};
in your main production code:
import { createApolloServer } from './createApolloServer';
// const authenticationStore = up to you
const apolloServer = createApolloServer(authenticationStore)
and in your test:
import { createApolloServer, createContext } from './createApolloServer';
it('should work ', async () => {
const authenticationStore = {
set: jest.fn()
};
const mockCreateContext = () => {
const req = {
profile: {
id: 123
}
};
return createContext(authenticationStore)({ req });
};
const apolloServer = createApolloServer(authenticationStore, mockCreateContext);
const { query } = createTestClient(apolloServer);
// etc.
});
Hi, is there a proper solution atm? It is crazy that there is no supported way to pass custom headers to the req.
@FrancescoSaverioZuppichini I've implemented a custom testClient. Check it out
Nice, thank you @KristianWEB
@KristianWEB this has also helped us with our ApolloServer testing. Thank you!
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 }
});
}
So, for those like me who came here looking for how to do this who use TS and don't use express, I ended up writing my own stub for the context function. It's really not ideal and I'm still beyond shocked that something so basic isn't handled, but you gotta work with what you've got, so here we are:
// wherever_you_are_creating_your_apollo_server.ts
export interface Context {
currentUser?: IUser;
}
export async function createApolloServer(
findUserContext?: (req: any) => Promise<Context>
) {
const schema = await buildSchema({ ... });
const contextToUse = findUserContext || validateUser;
const apolloServer = new ApolloServer({
schema,
...
context: async ({ req }) => {
return await contextToUse(req);
},
});
return apolloServer;
}
export const validateUser = async (req: any) => {
return findUserWithRequest(req);
};
export const findUserWithRequest = async (req: any): Promise<Context> => {
// Find your user here
const headers = req?.headers["cookie/auth/token/whatever_youre_using"]
const currentUser = User.findFromHeaders(headers)
return { currentUser }
}
// my_handy_spec_helper.ts
import { createApolloServer, findUserWithRequest } from '~/server
export async function setupAuthTestApolloClient(token: string) {
const apolloServer = await createApolloServer(findUserForToken(token));
const { query, mutate } = createTestClient(apolloServer);
return {
query,
mutate,
};
}
const findUserForToken = (token: String) => async (req: any) => {
const headers = req?.headers || [];
// construct your stubbed headers here
headers['token'] = token
const stubbedReq = {
...req,
headers: headers,
};
return findUserWithRequest(stubbedReq);
};
// my_important_spec.spec.ts
import {
setupTestDBConnection,
setupTestApolloClient,
} from "../../utils/helpers";
describe("User queries", () => {
it("can fetch the currently logged in user", async () => {
const connection = await setupTestDBConnection();
const email = "[email protected]";
const password = await hashPassword("battery_horse_staple");
await createUser({ email, password });
const token = generateJwtToken({ email });
const { query } = await setupAuthTestApolloClient(token);
try {
const res = await query({
query: GET_ME,
});
const loggedInUser: IUser = res?.data?.me;
expect(loggedInUser.email).toEqual(email);
} finally {
connection.close();
}
});
});
With this setup you can
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.
Similar to @Aaronik's solution (https://github.com/apollographql/apollo-server/issues/2277#issuecomment-586042231), this works fine for my use-case. I'm using type-graphql and have a custom auth guard that handles jwt verification.
_createApolloTestServer.ts_
import { ApolloServer } from "apollo-server-micro";
import { createSchema } from "pages/api/graphql";
export const createApolloTestServer = async (headers = {} as any) => {
const schema = await createSchema();
const server = new ApolloServer({
schema,
debug: true,
context: ({ req, res }) => {
return {
res,
req: {
...req,
headers
}
};
}
});
return server;
};
_testSomthing.spec.ts_
import { createTestClient } from "apollo-server-testing";
import { createApolloTestServer } from "test-utils/createApolloTestServer";
beforeAll(async () => {
apolloServer = await createApolloTestServer({
authorization: `Bearer ${access_token}`
});
});
it("does something", async () => {
const { mutate } = createTestClient(apolloServer);
// req: { headers: { authorization: "..." } } will be accessible in the resolver
const response = await mutate({
mutation: GQL_MUTATION,
variables: {
...mutationVariables
}
});
});
We are still experiencing this issue.
We are still experiencing this issue.
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
GraphQLResponse to give more type information while testinghttps://gist.github.com/preetjdp/178643c5854ae775b005834be6687edc
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
- Allows to pass headers while making an operation, this is in contrast to how headers are set in Apollo-Server-Integration-Testing
- Changed the operation response type to
GraphQLResponseto give more type information while testinghttps://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.
Thanks for the heads up @dipiash, I faced this last week myself,
I've updated the gist as well
Hey friends, maybe I don't see the problem, but what stops you from creating a
pipeline to your actual context function?
import { JWT_SECRET } from './config.js'
import jwt from 'jsonwebtoken'
// export your context function from a file and import it in your tests
export default function ({ req }) {
let token = req.headers.authorization || ''
token = token.replace('Bearer ', '')
try {
return jwt.verify(token, JWT_SECRET)
} catch (e) {
return {}
}
}
You can construct a fake express.Request object and pipe it to your context
function. I call this fake object reqMock.
Of course it would be nice to import a constructor from express module in
order to create a real instance of express.Request.
import { createTestClient } from 'apollo-server-testing'
import { GraphQLError } from 'graphql'
import schema from '../schema'
import { ApolloServer, gql } from 'apollo-server'
import context from '../context' // this is your context
let reqMock
let resMock
const contextPipeline = () => context({ req: reqMock, res: resMock })
const server = new ApolloServer({ schema, context: contextPipeline })
beforeEach(() => {
// For me a mock `express.Request` containing some headers is enough.
// I assume typescript will complain here:
reqMock = { headers: {} }
resMock = {}
})
describe('mutations', () => {
describe('write', () => {
const opts = {
mutation: gql`
mutation($postInput: PostInput!) {
write(post: $postInput) {
id
title
author {
id
}
}
}
`,
variables: { postInput: { title: 'New post' } }
}
describe('unauthenticated', () => {
beforeEach(() => {
reqMock = { headers: {} }
})
it('throws authorization error', async () => {
await expect(mutate(opts)).resolves.toMatchObject({
data: { write: null },
errors: [
new GraphQLError('Not Authorised!')
]
})
})
})
describe('authenticated', () => {
beforeEach(() => {
reqMock = {
headers: {
authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFsaWNlIiwiaWF0IjoxNjA2OTQyMzc4fQ.RSXUAq7IVrxb3WeIMJ3pBmszjJzmFBP97h-pINbi3sc'
}
}
})
it('returns created post', async () => {
await expect(mutate(opts)).resolves.toMatchObject({
errors: undefined,
data: {
write: {
id: expect.any(String),
title: 'New post',
author: { id: 'alice' }
}
}
})
})
})
})
})
Any plans to implement this? I'm also experiencing testing limitations due to this issue
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!
Most helpful comment
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.