Skip to content

Commit cd779db

Browse files
authored
Deprecate apollo-server-testing; allow ASTs for executeOperation (#5238)
The `apollo-server-testing` package exports one small function which is just a tiny wrapper around `server.executeOperation`. The one main advantage it provides is that you can pass in operations as ASTs rather than only as strings. This extra layer doesn't add much value but does require us to update things in two places (which cross a package barrier and thus can be installed at skewed versions). So for example when adding the second argument to `executeOperation` in #4166 I did not bother to add it to `apollo-server-testing` too. We've also found that users have been confused by the `createTestClient` API (eg #5111) and that some linters get confused by the unbound methods it returns (#4724). So the simplest thing is to just teach people how to use the real `ApolloServer` method instead of an unrelated API. This PR allows you to pass an AST to `server.executeOperation` (just like with the `apollo-server-testing` API), and changes the docs to recommend `executeOperation` instead of `apollo-server-testing`. It also makes some other suggestions about how to test Apollo Server code in a more end-to-end fashion, and adds some basic tests for `executeOperation`. Fixes #4952.
1 parent df92f39 commit cd779db

5 files changed

Lines changed: 146 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ The version headers in this history reflect the versions of Apollo Server itself
1313
1414
- `apollo-server-core`: Fix a race condition where schema reporting could lead to a delay at process shutdown. [PR #5222](https://github.com/apollographql/apollo-server/pull/5222)
1515
- `apollo-server-core`: Allow the Fetch API implementation to be overridden for the schema reporting and usage reporting plugins via a new `fetcher` option. [PR #5179](https://github.com/apollographql/apollo-server/pull/5179)
16+
- `apollo-server-core`: The `server.executeOperation` method (designed for testing) can now take its `query` as a `DocumentNode` (eg, a `gql`-tagged string) in addition to as a string. (This matches the behavior of the `apollo-server-testing` `createTestClient` function which is now deprecated.) We now recommend this method instead of `apollo-server-testing` in our docs. [Issue #4952](https://github.com/apollographql/apollo-server/issues/4952)
17+
- `apollo-server-testing`: Replace README with a deprecation notice explaining how to use `server.executeOperation` instead. [Issue #4952](https://github.com/apollographql/apollo-server/issues/4952)
1618

1719
## v2.24.1
1820

docs/source/testing/testing.md

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,26 @@ title: Integration testing
33
description: Utilities for testing Apollo Server
44
---
55

6-
Testing `apollo-server` can be done in many ways. The `apollo-server-testing` package provides tooling to make testing easier and accessible to users of all of the `apollo-server` integrations.
6+
Testing `apollo-server` can be done in many ways. One simple way is to use ApolloServer's `executeOperation` method to directly execute a GraphQL operation without going through a full HTTP operation.
77

8-
## `createTestClient`
8+
## `executeOperation`
99

10-
Integration testing a GraphQL server means testing many things. `apollo-server` has a request pipeline that can support many plugins that can affect the way an operation is executed. `createTestClient` provides a single hook to run operations through the request pipeline, enabling the most thorough tests possible without starting up an HTTP server.
10+
Integration testing a GraphQL server means testing many things. `apollo-server` has a request pipeline that can support many plugins that can affect the way an operation is executed. The `executeOperation` method provides a single hook to run operations through the request pipeline, enabling the most thorough tests possible without starting up an HTTP server.
1111

1212
```javascript
13-
const { createTestClient } = require('apollo-server-testing');
14-
15-
const { query, mutate } = createTestClient(server);
13+
const server = new ApolloServer(config);
1614

17-
query({
15+
const result = await server.executeOperation({
1816
query: GET_USER,
1917
variables: { id: 1 }
2018
});
21-
22-
mutate({
23-
mutation: UPDATE_USER,
24-
variables: { id: 1, email: 'nancy@foo.co' }
25-
});
19+
expect(result.errors).toBeUndefined();
20+
expect(result.data?.user.name).toBe('Ida');
2621
```
2722
28-
When passed an instance of the `ApolloServer` class, `createTestClient` returns a `query` and `mutate` function that can be used to run operations against the server instance. Currently, queries and mutations are the only operation types supported by `createTestClient`.
23+
For example, you can set up a full server with your schema and resolvers and run an operation against it.
2924
3025
```javascript
31-
const { createTestClient } = require('apollo-server-testing');
32-
3326
it('fetches single launch', async () => {
3427
const userAPI = new UserAPI({ store });
3528
const launchAPI = new LaunchAPI();
@@ -42,6 +35,7 @@ it('fetches single launch', async () => {
4235
dataSources: () => ({ userAPI, launchAPI }),
4336
context: () => ({ user: { id: 1, email: 'a@a.a' } }),
4437
});
38+
await server.start();
4539

4640
// mock the dataSource's underlying fetch methods
4741
launchAPI.get = jest.fn(() => [mockLaunchResponse]);
@@ -50,15 +44,46 @@ it('fetches single launch', async () => {
5044
{ dataValues: { launchId: 1 } },
5145
]);
5246

53-
// use the test server to create a query function
54-
const { query } = createTestClient(server);
55-
5647
// run query against the server and snapshot the output
57-
const res = await query({ query: GET_LAUNCH, variables: { id: 1 } });
48+
const res = await server.executeOperation({ query: GET_LAUNCH, variables: { id: 1 } });
5849
expect(res).toMatchSnapshot();
5950
});
6051
```
6152
62-
This is an example of a full integration test being run against a test instance of `apollo-server`. This test imports the important pieces to test (`typeDefs`, `resolvers`, `dataSources`) and creates a new instance of `apollo-server`. Once an instance is created, it's passed to `createTestClient` which returns `{ query, mutate }`. These methods can then be used to execute operations against the server.
53+
This is an example of a full integration test being run against a test instance of `apollo-server`. This test imports the important pieces to test (`typeDefs`, `resolvers`, `dataSources`) and creates a new instance of `apollo-server`.
54+
55+
The example above shows writing a test-specific [`context` function](../data/resolvers/#the-context-argument) which provides data directly instead of calculating it from the request context. If you'd like to use your server's real `context` function, you can pass a second argument to `executeOperation` which will be passed to your `context` function as its argument. You will need to put to gether an object with the [middleware-specific context fields](../api/apollo-server/#middleware-specific-context-fields) yourself.
56+
57+
You can use `executeOperation` to execute queries and mutations. Because the interface matches the GraphQL HTTP protocol, you specify the operation text under the `query` key even if the operation is a mutation. You can specify `query` either as a string or as a `DocumentNode` (an AST created by the `gql` tag).
58+
59+
In addition to `query`, the first argument to `executeOperation` can take `operationName`, `variables`, `extensions`, and `http` keys.
60+
61+
Note that errors in parsing, validating, and executing your operation are returned in the `errors` field of the result (just like in a GraphQL response) rather than thrown.
62+
63+
## `createTestClient` and `apollo-server-testing`
64+
65+
There is also a package called `apollo-server-testing` which exports a function `createTestClient` which wraps `executeOperation`. This API does not support the second context-function-argument argument, and doesn't provide any real advantages over calling `executeOperation` directly. It is deprecated and will no longer be published with Apollo Server 3.
66+
67+
We recommend that you replace this code:
68+
69+
```js
70+
const { createTestClient } = require('apollo-server-testing');
71+
72+
const { query, mutate } = createTestClient(server);
73+
74+
await query({ query: QUERY });
75+
await mutate({ mutation: MUTATION });
76+
```
77+
78+
with
79+
80+
```js
81+
await server.executeOperation({ query: QUERY });
82+
await server.executeOperation({ query: MUTATION });
83+
```
84+
85+
## End-to-end testing
86+
87+
Instead of bypassing the HTTP layer, you may just want to fully run your server and test it with a real HTTP client.
6388
64-
For more examples of this tool in action, check out the [integration tests](https://github.com/apollographql/fullstack-tutorial/blob/master/final/server/src/__tests__/integration.js) in the [Fullstack Tutorial](https://www.apollographql.com/docs/tutorial/introduction.html).
89+
Apollo Server doesn't have any built-in support for this. You can combine any HTTP or GraphQL client such as [`supertest`](https://www.npmjs.com/package/supertest) or [Apollo Client's HTTP Link](https://www.apollographql.com/docs/react/api/link/apollo-link-http/) to run operations against your server. There are also community packages available such as [`apollo-server-integration-testing`](https://www.npmjs.com/package/apollo-server-integration-testing) which provides an API similar to the deprecated `apollo-server-testing` package which uses mocked Express request and response objects.

packages/apollo-server-core/src/ApolloServer.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ValidationContext,
2020
FieldDefinitionNode,
2121
DocumentNode,
22+
print,
2223
} from 'graphql';
2324
import resolvable, { Resolvable } from '@josephg/resolvable';
2425
import { GraphQLExtension } from 'graphql-extensions';
@@ -1265,6 +1266,11 @@ export class ApolloServerBase {
12651266
* going through the HTTP layer. Note that this means that any handling you do
12661267
* in your server at the HTTP level will not affect this call!
12671268
*
1269+
* For convenience, you can provide `request.query` either as a string or a
1270+
* DocumentNode, in case you choose to use the gql tag in your tests. This is
1271+
* just a convenience, not an optimization (we convert provided ASTs back into
1272+
* string).
1273+
*
12681274
* If you pass a second argument to this method and your ApolloServer's
12691275
* `context` is a function, that argument will be passed directly to your
12701276
* `context` function. It is your responsibility to make it as close as needed
@@ -1273,7 +1279,12 @@ export class ApolloServerBase {
12731279
* `{req: express.Request, res: express.Response }` object) and to keep it
12741280
* updated as you upgrade Apollo Server.
12751281
*/
1276-
public async executeOperation(request: GraphQLRequest, integrationContextArgument?: Record<string, any>) {
1282+
public async executeOperation(
1283+
request: Omit<GraphQLRequest, 'query'> & {
1284+
query?: string | DocumentNode;
1285+
},
1286+
integrationContextArgument?: Record<string, any>,
1287+
) {
12771288
const options = await this.graphQLServerOptions(integrationContextArgument);
12781289

12791290
if (typeof options.context === 'function') {
@@ -1292,7 +1303,13 @@ export class ApolloServerBase {
12921303
logger: this.logger,
12931304
schema: options.schema,
12941305
schemaHash: options.schemaHash,
1295-
request,
1306+
request: {
1307+
...request,
1308+
query:
1309+
request.query && typeof request.query !== 'string'
1310+
? print(request.query)
1311+
: request.query,
1312+
},
12961313
context: options.context || Object.create(null),
12971314
cache: options.cache!,
12981315
metrics: {},

packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApolloServerBase } from '../ApolloServer';
22
import { buildServiceDefinition } from '@apollographql/apollo-tools';
3-
import gql from 'graphql-tag';
3+
import { gql } from '../';
44
import { Logger } from 'apollo-server-types';
55
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
66
import type { GraphQLSchema } from 'graphql';
@@ -9,6 +9,7 @@ const typeDefs = gql`
99
type Query {
1010
hello: String
1111
error: Boolean
12+
contextFoo: String
1213
}
1314
`;
1415

@@ -20,6 +21,9 @@ const resolvers = {
2021
error() {
2122
throw new Error('A test error');
2223
},
24+
contextFoo(_root: any, _args: any, context: any) {
25+
return context.foo;
26+
},
2327
},
2428
};
2529

@@ -181,6 +185,60 @@ describe('ApolloServerBase executeOperation', () => {
181185
expect(result.errors?.[0].extensions?.code).toBe('INTERNAL_SERVER_ERROR');
182186
expect(result.errors?.[0].extensions?.exception?.stacktrace).toBeDefined();
183187
});
188+
189+
it('works with string', async () => {
190+
const server = new ApolloServerBase({
191+
typeDefs,
192+
resolvers,
193+
});
194+
195+
const result = await server.executeOperation({ query: '{ hello }' });
196+
expect(result.errors).toBeUndefined();
197+
expect(result.data?.hello).toBe('world');
198+
});
199+
200+
it('works with AST', async () => {
201+
const server = new ApolloServerBase({
202+
typeDefs,
203+
resolvers,
204+
});
205+
206+
const result = await server.executeOperation({
207+
query: gql`
208+
{
209+
hello
210+
}
211+
`,
212+
});
213+
expect(result.errors).toBeUndefined();
214+
expect(result.data?.hello).toBe('world');
215+
});
216+
217+
it('parse errors', async () => {
218+
const server = new ApolloServerBase({
219+
typeDefs,
220+
resolvers,
221+
});
222+
223+
const result = await server.executeOperation({ query: '{' });
224+
expect(result.errors).toHaveLength(1);
225+
expect(result.errors?.[0].extensions?.code).toBe('GRAPHQL_PARSE_FAILED');
226+
});
227+
228+
it('passes its second argument to context function', async () => {
229+
const server = new ApolloServerBase({
230+
typeDefs,
231+
resolvers,
232+
context: ({ fooIn }) => ({ foo: fooIn }),
233+
});
234+
235+
const result = await server.executeOperation(
236+
{ query: '{ contextFoo }' },
237+
{ fooIn: 'bla' },
238+
);
239+
expect(result.errors).toBeUndefined();
240+
expect(result.data?.contextFoo).toBe('bla');
241+
});
184242
});
185243

186244
describe('environment variables', () => {
Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
# apollo-server-testing
22

3-
[![npm version](https://badge.fury.io/js/apollo-server-testing.svg)](https://badge.fury.io/js/apollo-server-testing)
4-
[![Build Status](https://circleci.com/gh/apollographql/apollo-server/tree/main.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-server)
3+
This deprecated package contains a function `createTestClient` which is a very thin wrapper around the Apollo Server `server.executeOperation` method.
54

6-
This is the testing module of the Apollo community GraphQL Server. [Read the docs.](https://www.apollographql.com/docs/apollo-server/)
7-
[Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md)
5+
Code that uses this package looks like the following, where `server` is an `ApolloServer`:
6+
7+
```js
8+
const { createTestClient } = require('apollo-server-testing');
9+
10+
const { query, mutate } = createTestClient(server);
11+
12+
await query({ query: QUERY });
13+
await mutate({ mutation: MUTATION });
14+
```
15+
16+
We recommend you stop using this package and replace the above code with the equivalent:
17+
18+
```js
19+
await server.executeOperation({ query: QUERY });
20+
await server.executeOperation({ query: MUTATION });
21+
```
22+
23+
This package will not be distributed as part of Apollo Server 3.

0 commit comments

Comments
 (0)