diff --git a/CHANGELOG.md b/CHANGELOG.md index 97460751468..57348fe2923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The version headers in this history reflect the versions of Apollo Server itself - `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) - `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) +- `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) +- `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) ## v2.24.1 diff --git a/docs/source/testing/testing.md b/docs/source/testing/testing.md index e5ea248ca65..af1e31cde7e 100644 --- a/docs/source/testing/testing.md +++ b/docs/source/testing/testing.md @@ -3,33 +3,26 @@ title: Integration testing description: Utilities for testing Apollo Server --- -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. +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. -## `createTestClient` +## `executeOperation` -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. +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. ```javascript -const { createTestClient } = require('apollo-server-testing'); - -const { query, mutate } = createTestClient(server); +const server = new ApolloServer(config); -query({ +const result = await server.executeOperation({ query: GET_USER, variables: { id: 1 } }); - -mutate({ - mutation: UPDATE_USER, - variables: { id: 1, email: 'nancy@foo.co' } -}); +expect(result.errors).toBeUndefined(); +expect(result.data?.user.name).toBe('Ida'); ``` -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`. +For example, you can set up a full server with your schema and resolvers and run an operation against it. ```javascript -const { createTestClient } = require('apollo-server-testing'); - it('fetches single launch', async () => { const userAPI = new UserAPI({ store }); const launchAPI = new LaunchAPI(); @@ -42,6 +35,7 @@ it('fetches single launch', async () => { dataSources: () => ({ userAPI, launchAPI }), context: () => ({ user: { id: 1, email: 'a@a.a' } }), }); + await server.start(); // mock the dataSource's underlying fetch methods launchAPI.get = jest.fn(() => [mockLaunchResponse]); @@ -50,15 +44,46 @@ it('fetches single launch', async () => { { dataValues: { launchId: 1 } }, ]); - // use the test server to create a query function - const { query } = createTestClient(server); - // run query against the server and snapshot the output - const res = await query({ query: GET_LAUNCH, variables: { id: 1 } }); + const res = await server.executeOperation({ query: GET_LAUNCH, variables: { id: 1 } }); expect(res).toMatchSnapshot(); }); ``` -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. +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`. + +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. + +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). + +In addition to `query`, the first argument to `executeOperation` can take `operationName`, `variables`, `extensions`, and `http` keys. + +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. + +## `createTestClient` and `apollo-server-testing` + +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. + +We recommend that you replace this code: + +```js +const { createTestClient } = require('apollo-server-testing'); + +const { query, mutate } = createTestClient(server); + +await query({ query: QUERY }); +await mutate({ mutation: MUTATION }); +``` + +with + +```js +await server.executeOperation({ query: QUERY }); +await server.executeOperation({ query: MUTATION }); +``` + +## End-to-end testing + +Instead of bypassing the HTTP layer, you may just want to fully run your server and test it with a real HTTP client. -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). +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. diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 6fbc91500b8..77fd03b4e1d 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -19,6 +19,7 @@ import { ValidationContext, FieldDefinitionNode, DocumentNode, + print, } from 'graphql'; import resolvable, { Resolvable } from '@josephg/resolvable'; import { GraphQLExtension } from 'graphql-extensions'; @@ -1265,6 +1266,11 @@ export class ApolloServerBase { * going through the HTTP layer. Note that this means that any handling you do * in your server at the HTTP level will not affect this call! * + * For convenience, you can provide `request.query` either as a string or a + * DocumentNode, in case you choose to use the gql tag in your tests. This is + * just a convenience, not an optimization (we convert provided ASTs back into + * string). + * * If you pass a second argument to this method and your ApolloServer's * `context` is a function, that argument will be passed directly to your * `context` function. It is your responsibility to make it as close as needed @@ -1273,7 +1279,12 @@ export class ApolloServerBase { * `{req: express.Request, res: express.Response }` object) and to keep it * updated as you upgrade Apollo Server. */ - public async executeOperation(request: GraphQLRequest, integrationContextArgument?: Record) { + public async executeOperation( + request: Omit & { + query?: string | DocumentNode; + }, + integrationContextArgument?: Record, + ) { const options = await this.graphQLServerOptions(integrationContextArgument); if (typeof options.context === 'function') { @@ -1292,7 +1303,13 @@ export class ApolloServerBase { logger: this.logger, schema: options.schema, schemaHash: options.schemaHash, - request, + request: { + ...request, + query: + request.query && typeof request.query !== 'string' + ? print(request.query) + : request.query, + }, context: options.context || Object.create(null), cache: options.cache!, metrics: {}, diff --git a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts index 1ef355f4608..4f5e43ff1b7 100644 --- a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts +++ b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts @@ -1,6 +1,6 @@ import { ApolloServerBase } from '../ApolloServer'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; -import gql from 'graphql-tag'; +import { gql } from '../'; import { Logger } from 'apollo-server-types'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import type { GraphQLSchema } from 'graphql'; @@ -9,6 +9,7 @@ const typeDefs = gql` type Query { hello: String error: Boolean + contextFoo: String } `; @@ -20,6 +21,9 @@ const resolvers = { error() { throw new Error('A test error'); }, + contextFoo(_root: any, _args: any, context: any) { + return context.foo; + }, }, }; @@ -181,6 +185,60 @@ describe('ApolloServerBase executeOperation', () => { expect(result.errors?.[0].extensions?.code).toBe('INTERNAL_SERVER_ERROR'); expect(result.errors?.[0].extensions?.exception?.stacktrace).toBeDefined(); }); + + it('works with string', async () => { + const server = new ApolloServerBase({ + typeDefs, + resolvers, + }); + + const result = await server.executeOperation({ query: '{ hello }' }); + expect(result.errors).toBeUndefined(); + expect(result.data?.hello).toBe('world'); + }); + + it('works with AST', async () => { + const server = new ApolloServerBase({ + typeDefs, + resolvers, + }); + + const result = await server.executeOperation({ + query: gql` + { + hello + } + `, + }); + expect(result.errors).toBeUndefined(); + expect(result.data?.hello).toBe('world'); + }); + + it('parse errors', async () => { + const server = new ApolloServerBase({ + typeDefs, + resolvers, + }); + + const result = await server.executeOperation({ query: '{' }); + expect(result.errors).toHaveLength(1); + expect(result.errors?.[0].extensions?.code).toBe('GRAPHQL_PARSE_FAILED'); + }); + + it('passes its second argument to context function', async () => { + const server = new ApolloServerBase({ + typeDefs, + resolvers, + context: ({ fooIn }) => ({ foo: fooIn }), + }); + + const result = await server.executeOperation( + { query: '{ contextFoo }' }, + { fooIn: 'bla' }, + ); + expect(result.errors).toBeUndefined(); + expect(result.data?.contextFoo).toBe('bla'); + }); }); describe('environment variables', () => { diff --git a/packages/apollo-server-testing/README.md b/packages/apollo-server-testing/README.md index b661a5cd059..fde8c2207be 100644 --- a/packages/apollo-server-testing/README.md +++ b/packages/apollo-server-testing/README.md @@ -1,7 +1,23 @@ # apollo-server-testing -[![npm version](https://badge.fury.io/js/apollo-server-testing.svg)](https://badge.fury.io/js/apollo-server-testing) -[![Build Status](https://circleci.com/gh/apollographql/apollo-server/tree/main.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-server) +This deprecated package contains a function `createTestClient` which is a very thin wrapper around the Apollo Server `server.executeOperation` method. -This is the testing module of the Apollo community GraphQL Server. [Read the docs.](https://www.apollographql.com/docs/apollo-server/) -[Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md) +Code that uses this package looks like the following, where `server` is an `ApolloServer`: + +```js +const { createTestClient } = require('apollo-server-testing'); + +const { query, mutate } = createTestClient(server); + +await query({ query: QUERY }); +await mutate({ mutation: MUTATION }); +``` + +We recommend you stop using this package and replace the above code with the equivalent: + +```js +await server.executeOperation({ query: QUERY }); +await server.executeOperation({ query: MUTATION }); +``` + +This package will not be distributed as part of Apollo Server 3.