From qa-graphql
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. Composes introspection-attack-surface-reference + persisted-query-strategy-reference for the production-safety assertions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-graphql:apollo-server-testThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Per
Per
apollographql.com/docs/apollo-server/testing/testing,
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.
executeOperation.supertest.introspection-attack-surface-reference.npm install --save-dev @apollo/server supertest @types/supertest
executeOperationPer Apollo docs:
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' },
});
// Per Apollo docs: "Any errors in parsing, validating, and
// executing your GraphQL operation are returned in the nested
// `errors` field" rather than being 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!');
});
const res = await testServer.executeOperation(
{ query: GET_LAUNCH, variables: { id: 1 } },
{
contextValue: {
user: { id: 1, email: '[email protected]' },
dataSources: { userAPI, launchAPI },
},
},
);
Pass contextValue to bypass the production
context-initialisation function (which usually parses headers).
Per Apollo docs:
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();
});
npm test # jest / vitest pick up *.test.ts
npx jest schema.test.ts -t "introspection"
Per
introspection-attack-surface-reference:
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);
});
Per
persisted-query-strategy-reference
Mode 2:
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/);
});
Per Apollo docs, 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.
# .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-pattern | Why it fails | Fix |
|---|---|---|
Tests in NODE_ENV=test only | Production defaults differ; introspection-disabled gate untested | Separate prod-config test job |
executeOperation for HTTP-layer concerns | Skips middleware, CORS, headers | Use supertest for HTTP-layer |
Hardcoded port 4000 in tests | Parallel CI conflicts | port: 0 |
Forgetting server.stop() in afterAll | Goroutine / connection leak across tests | Always stop |
Asserting errors[0].message string | Brittle to wording / i18n | Assert extensions.code |
Skipping contextValue injection | Tests use real auth headers; flaky | Inject mocked context per test |
| One mega-test for the whole schema | Failures hard to diagnose | One test per resolver / operation |
data access without checking errors | Errors masked; false positives | Check errors === undefined first |
executeOperation skips HTTP. Auth via headers, CORS,
rate-limiting middleware are not exercised. Use supertest for
those.graphql-ws test patterns.qa-contract-testing/graphql-schema-regression.introspection-attack-surface-reference,
persisted-query-strategy-reference.graphql-yoga-test,
hasura-test,
mercurius-test,
pothos-builder-tests.n-plus-one-query-detector.qa-contract-testing/graphql-schema-regression.Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub testland/qa --plugin qa-graphql