CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/apollo-server-tests

Wraps Apollo Server testing patterns: `server.executeOperation()` (in-process, no HTTP), `supertest` against an ephemeral-port HTTP server (port 0), context injection via the `contextValue` second-argument, and assertion patterns for response shape + errors. Includes the production-config gates testable through this skill - introspection-disabled, persisted-query mode, hideSchemaDetailsFromClientErrors. Use when writing tests for an Apollo Server v4+ GraphQL service.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
apollo-server-tests
description:
Wraps Apollo Server testing patterns: `server.executeOperation()` (in-process, no HTTP), `supertest` against an ephemeral-port HTTP server (port 0), context injection via the `contextValue` second-argument, and assertion patterns for response shape + errors. Includes the production-config gates testable through this skill - introspection-disabled, persisted-query mode, hideSchemaDetailsFromClientErrors. Use when writing tests for an Apollo Server v4+ GraphQL service.

apollo-server-tests

Overview

executeOperation initializes automatically - no startup needed for unit-style tests against the schema in-process. For HTTP-layer tests (CORS, middleware, response headers), use supertest against an ephemeral-port server.

When to use

  • Unit tests for resolvers using executeOperation.
  • Integration tests for HTTP-layer behaviour using supertest.
  • Production-config gates: assert introspection / APQ / hideSchemaDetails settings match your production config.

Authoring

Install

npm install --save-dev @apollo/server supertest @types/supertest

In-process tests with executeOperation

import { ApolloServer } from '@apollo/server';
import { typeDefs, resolvers } from './schema';

const testServer = new ApolloServer({ typeDefs, resolvers });

test('returns greeting', async () => {
  const response = await testServer.executeOperation({
    query: 'query SayHelloWorld($name: String) { hello(name: $name) }',
    variables: { name: 'world' },
  });

  // Parse/validation/execution errors surface in `errors`, not thrown
  if (response.body.kind !== 'single') throw new Error('expected single');
  expect(response.body.singleResult.errors).toBeUndefined();
  expect(response.body.singleResult.data?.hello).toBe('Hello world!');
});

Context injection (auth, datasources)

const res = await testServer.executeOperation(
  { query: GET_LAUNCH, variables: { id: 1 } },
  {
    contextValue: {
      user: { id: 1, email: 'a@a.a' },
      dataSources: { userAPI, launchAPI },
    },
  },
);

contextValue bypasses the production context function (which parses headers).

HTTP-layer tests with supertest

import { startStandaloneServer } from '@apollo/server/standalone';
import request from 'supertest';

let server: ApolloServer;
let url: string;

beforeAll(async () => {
  server = new ApolloServer({ typeDefs, resolvers });
  ({ url } = await startStandaloneServer(server, {
    listen: { port: 0 },   // OS picks port → parallel-test safe
  }));
});

afterAll(async () => {
  await server?.stop();
});

it('says hello over HTTP', async () => {
  const response = await request(url)
    .post('/')
    .send({ query: '{ hello }' });
  expect(response.status).toBe(200);
  expect(response.body.data?.hello).toBeDefined();
});

Running

Standard test commands

npm test                    # jest / vitest pick up *.test.ts
npx jest schema.test.ts -t "introspection"

Production-config tests (most important)

Assert prod-time security defaults under NODE_ENV=production:

import { ApolloServer } from '@apollo/server';

test('introspection disabled when production', async () => {
  process.env.NODE_ENV = 'production';
  const prodServer = new ApolloServer({
    typeDefs, resolvers,
    introspection: process.env.NODE_ENV !== 'production',
  });
  const resp = await prodServer.executeOperation({
    query: '{ __schema { types { name } } }',
  });
  if (resp.body.kind !== 'single') throw new Error('expected single');
  expect(resp.body.singleResult.errors).toBeDefined();
  expect(resp.body.singleResult.errors?.[0].message).toMatch(/introspection/i);
});

test('hideSchemaDetailsFromClientErrors strips did-you-mean', async () => {
  const server = new ApolloServer({
    typeDefs, resolvers,
    hideSchemaDetailsFromClientErrors: true,
  });
  const resp = await server.executeOperation({
    query: '{ usre { id } }',  // typo
  });
  if (resp.body.kind !== 'single') throw new Error('expected single');
  expect(JSON.stringify(resp.body.singleResult.errors)).not.toMatch(/did you mean/i);
});

Persisted-query mode test

Strict allowlist (APQ) mode:

test('strict APQ rejects unregistered hash', async () => {
  const server = new ApolloServer({
    typeDefs, resolvers,
    persistedQueries: false,  // turn off auto-register
    plugins: [/* strict-allowlist plugin from manifest */],
  });
  const { url } = await startStandaloneServer(server, { listen: { port: 0 } });

  const resp = await request(url).post('/').send({
    extensions: { persistedQuery: { version: 1, sha256Hash: 'deadbeef'.repeat(8) } },
  });
  expect(resp.body.errors[0].extensions.code).toMatch(/PERSISTED_QUERY/);
});

Parsing results

The executeOperation response shape is discriminated:

type ExecuteOperationResult =
  | { body: { kind: 'single'; singleResult: { data?: ...; errors?: ... } } }
  | { body: { kind: 'incremental'; ... } };

Always check kind === 'single' first. The errors array contains GraphQLError objects with message, path, extensions.code (e.g., 'BAD_USER_INPUT', 'UNAUTHENTICATED', 'PERSISTED_QUERY_NOT_FOUND').

For supertest: standard HTTP response; response.body.errors if GraphQL-level error, response.status if transport error.

CI integration

# .github/workflows/graphql-tests.yml
name: graphql
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
      - name: Production-config assertions
        env:
          NODE_ENV: production
        run: npx jest tests/production-config/ --forceExit

The production-config jobs run separately with NODE_ENV=production so the actual prod-time defaults are exercised.

Anti-patterns

Anti-patternWhy it failsFix
Tests in NODE_ENV=test onlyProduction defaults differ; introspection-disabled gate untestedSeparate prod-config test job
executeOperation for HTTP-layer concernsSkips middleware, CORS, headersUse supertest for HTTP-layer
Hardcoded port 4000 in testsParallel CI conflictsport: 0
Forgetting server.stop() in afterAllConnection leak across testsAlways stop
Asserting errors[0].message stringBrittle to wording / i18nAssert extensions.code
Skipping contextValue injectionTests use real auth headers; flakyInject mocked context per test
One mega-test for the whole schemaFailures hard to diagnoseOne test per resolver / operation
data access without checking errorsErrors masked; false positivesCheck errors === undefined first

Limitations

  • executeOperation skips HTTP. Auth via headers, CORS, rate-limiting middleware are not exercised. Use supertest for those.
  • Doesn't test subscriptions over WS. Subscriptions need a WebSocket server; use graphql-ws test patterns.
  • Doesn't catch type-level bugs at runtime. Test the runtime behaviour, not the type definitions.
  • Doesn't replace contract tests. Mocked context can drift from prod; pair with schema-regression contract tests.

References

  • Apollo Server testing: apollographql.com/docs/apollo-server/testing/testing
  • Apollo Server API - introspection, persisted queries, error config: apollographql.com/docs/apollo-server/api/apollo-server
Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/apollo-server-tests badge