Skip to content
Back to skills

Apollo Server Tests

ASecurity

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.

  • 10 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 12, 2026
testingtypescriptbashnodetestinggitapisecurity

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 12 files and shows the line behind each finding

Scanned October 12, 2026

npx -y skills add testland/qa --skill apollo-server-tests --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Apollo Server Tests?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Apollo Server Tests
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/testland-apollo-server-tests/badge)](https://www.skillsdirectory.com/skills/testland-apollo-server-tests)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

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

### In-process tests with `executeOperation`

```typescript
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)

```typescript
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

```typescript
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

```bash
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`:

```typescript
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:

```typescript
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:

```typescript
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

```yaml
# .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-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` | 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 |

## 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](https://www.apollographql.com/docs/apollo-server/testing/testing)
- Apollo Server API - introspection, persisted queries, error config:
  [apollographql.com/docs/apollo-server/api/apollo-server](https://www.apollographql.com/docs/apollo-server/api/apollo-server)

Files in this skill

  • .tessl-plugin/plugin.json606 B
  • SKILL.md7.6 KB
  • evals/launch-checklist-lands-on-the-suite/criteria.json6 KB
  • evals/launch-checklist-lands-on-the-suite/task.md8.8 KB
  • evals/one-test-called-the-api-works/criteria.json6.6 KB
  • evals/one-test-called-the-api-works/task.md9.9 KB
  • evals/preflight-blocked-after-the-web-app-moved-origin/criteria.json6.2 KB
  • evals/preflight-blocked-after-the-web-app-moved-origin/task.md8.6 KB
  • evals/security-questionnaire-on-settings-nobody-tested/criteria.json6.5 KB
  • evals/security-questionnaire-on-settings-nobody-tested/task.md7.4 KB
  • evals/three-hours-of-null-billing-and-a-green-job/criteria.json7.1 KB
  • evals/three-hours-of-null-billing-and-a-green-job/task.md6.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.