Testland
Browse all skills & agents

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill apollo-server-tests
View source

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

Related skills

graphql-complexity-limit-tester

The GraphQL attack-surface / hardening skill: crafts over-limit depth and complexity queries then asserts rejection before execution, verifying that graphql-depth-limit, graphql-cost-analysis, and graphql-armor (max-depth / cost-limit / max-tokens plugins) are actually enforced and not just configured; carries the introspection attack-surface catalog (what __schema exposes, per-framework disable controls, hideSchemaDetailsFromClientErrors) in references/introspection.md and the persisted-query allowlisting strategies (Apollo APQ protocol, auto-register vs strict-allowlist vs hybrid modes) in references/persisted-queries.md. Use when auditing a GraphQL service for DoS or schema-disclosure exposure, hardening a production deployment, or adding tests that prove the limits in CI.

graphql-n-plus-one-remediation

Detects and fixes the GraphQL N+1 pattern: scans a repo, PR diff, or schema type for list-returning resolvers (grep-driven detection workflow), traces the resolver tree to locate the fan-out (one parent query returns N rows, then a child field resolver fires once per row), classifies every child field resolver as safe or N+1 risk, and applies one of three fixes: per-request DataLoader batching, eager projection in the parent resolver, or selection-set-aware prefetch. Use when reviewing a PR that adds or changes a list-returning resolver, when a connection-pool exhaustion or slow-query alert traces back to GraphQL traffic, or when a resolver trace shows a child field resolved once per parent row.

graphql-subscription-test-author

Authors GraphQL subscription resolver test suites over graphql-ws (WebSocket) and graphql-sse (Server-Sent Events) transports: subscribe to event streams via the async-iterator API, assert emitted data shape and sequence, verify connection lifecycle and protocol close codes, and validate auth-on-connect (connectionParams / authenticate callback) plus resolver-level pubsub trigger logic. Use for real-time subscription operations; not for queries or mutations - for those use apollo-server-tests or graphql-yoga-tests (Mercurius in its references).

graphql-yoga-tests

Tests a GraphQL Yoga server (the-guild.dev runtime) with `yoga.fetch()` for in-process, no-network request simulation of queries and mutations, `@graphql-tools/executor-http` for subscription and incremental-delivery (streaming) tests, auth-header pass-through, and production-config gates for disabled introspection and persisted operations; also carries the Mercurius (Fastify GraphQL plugin) in-process `app.inject()` testing patterns in references/mercurius.md. Use to test a GraphQL Yoga or Mercurius server, write query, mutation, or subscription tests, or check production plugin config; for other runtimes use apollo-server-tests or hasura-tests instead, not this skill.

hasura-tests

Wraps Hasura GraphQL Engine testing patterns: docker-compose test instance, the metadata API for declarative schema/permission setup, x-hasura-role and x-hasura-user-id session headers for role-based permission tests, the v1/graphql endpoint via curl/HTTPie/native HTTP clients, and the role-by-table-by-operation permission-matrix pattern. Use for a metadata-driven Hasura engine where row-level permissions dominate; for a code-first server runtime harness use graphql-yoga-tests (Mercurius in its references) or apollo-server-tests instead, not this skill.