# Part 7: Testing, Performance, and Deployment

## Shipping GraphQL with Confidence

A GraphQL service that is well-tested and performance-profiled behaves predictably under production load. This part covers the full quality lifecycle: unit testing resolvers, integration testing with a real database, measuring and fixing N+1 regressions, adding query complexity limits, and deploying the federated supergraph.

## Testing the TypeScript Subgraph

### Unit Testing Resolvers with `executeOperation`

Apollo Server v4 ships a `executeOperation` method for testing without starting a real HTTP server. I use this for all resolver unit tests.

```bash
npm install --save-dev jest @types/jest ts-jest
```

```typescript
// jest.config.ts
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/*.test.ts'],
};
```

```typescript
// src/__tests__/products.test.ts
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { typeDefs } from '../schema';
import { resolvers } from '../resolvers';
import { AppContext } from '../context';

// ─── Minimal fake context ───────────────────────────────────────────────────

function buildTestContext(overrides: Partial<AppContext> = {}): AppContext {
  const prismaMock = {
    product: {
      findUnique: jest.fn(),
      findMany: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
      count: jest.fn(),
    },
    category: {
      findUnique: jest.fn(),
      findMany: jest.fn(),
    },
  };

  return {
    prisma: prismaMock as any,
    loaders: {
      category: { load: jest.fn() } as any,
      productVariants: { load: jest.fn() } as any,
    },
    user: { id: 'user-1', email: 'test@example.com', role: 'ADMIN' },
    ...overrides,
  };
}

// ─── Test Suite ─────────────────────────────────────────────────────────────

describe('Product resolvers', () => {
  let server: ApolloServer<AppContext>;

  beforeAll(async () => {
    server = new ApolloServer({
      schema: buildSubgraphSchema({ typeDefs, resolvers }),
    });
    await server.start();
  });

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

  it('returns a product by id', async () => {
    const productFixture = {
      id: 'prod-1',
      name: 'Wireless Keyboard',
      slug: 'wireless-keyboard',
      price: 79.99,
      currency: 'USD',
      stockCount: 5,
      categoryId: 'cat-1',
      tags: ['electronics'],
      status: 'PUBLISHED',
      description: null,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    const ctx = buildTestContext();
    (ctx.prisma.product.findUnique as jest.Mock).mockResolvedValue(productFixture);
    (ctx.loaders.category.load as jest.Mock).mockResolvedValue({
      id: 'cat-1',
      name: 'Electronics',
      slug: 'electronics',
    });

    const response = await server.executeOperation(
      {
        query: `
          query GetProduct($id: UUID!) {
            product(id: $id) {
              id
              name
              price
              inStock
              category { name }
            }
          }
        `,
        variables: { id: 'prod-1' },
      },
      { contextValue: ctx },
    );

    expect(response.body.kind).toBe('single');
    const data = (response.body as any).singleResult.data;

    expect(data.product.id).toBe('prod-1');
    expect(data.product.name).toBe('Wireless Keyboard');
    expect(data.product.inStock).toBe(true); // stockCount > 0
    expect(data.product.category.name).toBe('Electronics');
    expect(ctx.prisma.product.findUnique).toHaveBeenCalledWith({ where: { id: 'prod-1' } });
  });

  it('returns null for a non-existent product', async () => {
    const ctx = buildTestContext();
    (ctx.prisma.product.findUnique as jest.Mock).mockResolvedValue(null);

    const response = await server.executeOperation(
      { query: `query { product(id: "does-not-exist") { id name } }` },
      { contextValue: ctx },
    );

    const data = (response.body as any).singleResult.data;
    expect(data.product).toBeNull();
  });

  it('rejects createProduct without an editor role', async () => {
    const ctx = buildTestContext({
      user: { id: 'user-2', email: 'viewer@example.com', role: 'VIEWER' },
    });

    const response = await server.executeOperation(
      {
        query: `
          mutation {
            createProduct(input: {
              name: "Test Product"
              slug: "test-product"
              price: 9.99
              currency: "USD"
              categoryId: "cat-1"
            }) {
              id
            }
          }
        `,
      },
      { contextValue: ctx },
    );

    const errors = (response.body as any).singleResult.errors;
    expect(errors).toBeDefined();
    expect(errors[0].extensions.code).toBe('FORBIDDEN');
  });
});
```

### Integration Testing with Testcontainers

Unit tests with mocked Prisma are fast but do not catch query construction bugs. Integration tests run against a real PostgreSQL instance inside a Docker container.

```bash
npm install --save-dev testcontainers @testcontainers/postgresql
```

```typescript
// src/__tests__/integration/products.integration.test.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';

let prisma: PrismaClient;

beforeAll(async () => {
  const container = await new PostgreSqlContainer('postgres:16').start();

  process.env.DATABASE_URL = container.getConnectionUri();

  // Run migrations against the test container
  execSync('npx prisma migrate deploy', {
    env: { ...process.env, DATABASE_URL: container.getConnectionUri() },
  });

  prisma = new PrismaClient({
    datasources: { db: { url: container.getConnectionUri() } },
  });
}, 60_000); // allow up to 60s for Docker pull on first run

afterAll(async () => {
  await prisma.$disconnect();
});

it('creates and fetches a product with its category', async () => {
  const category = await prisma.category.create({
    data: { name: 'Electronics', slug: 'electronics' },
  });

  const product = await prisma.product.create({
    data: {
      name: 'Mechanical Keyboard',
      slug: 'mechanical-keyboard',
      price: 149.99,
      currency: 'USD',
      categoryId: category.id,
      tags: ['keyboard', 'peripherals'],
    },
  });

  const fetched = await prisma.product.findUnique({
    where: { id: product.id },
    include: { category: true },
  });

  expect(fetched?.name).toBe('Mechanical Keyboard');
  expect(fetched?.category.name).toBe('Electronics');
});
```

## Testing the Python Subgraph

Strawberry provides `schema.execute` for in-process testing without an HTTP server.

```python
# tests/test_inventory_schema.py
import pytest
from unittest.mock import AsyncMock, MagicMock
from strawberry.testing import build_field_extension_ctx
from src.schema import schema


@pytest.mark.asyncio
async def test_stock_level_query():
    mock_db = MagicMock()
    mock_db.get_stock_level = AsyncMock(return_value={
        "warehouse_id": "wh-1",
        "warehouse_name": "Main Warehouse",
        "quantity": 50,
        "reserved_quantity": 5,
        "available_quantity": 45,
        "updated_at": "2026-01-01T00:00:00",
    })

    result = await schema.execute(
        """
        query {
            stockLevel(productId: "prod-1", warehouseId: "wh-1") {
                warehouseName
                availableQuantity
            }
        }
        """,
        context_value={"db": mock_db, "user": None},
    )

    assert result.errors is None
    assert result.data["stockLevel"]["warehouseName"] == "Main Warehouse"
    assert result.data["stockLevel"]["availableQuantity"] == 45
    mock_db.get_stock_level.assert_called_once_with(product_id="prod-1", warehouse_id="wh-1")


@pytest.mark.asyncio
async def test_unauthenticated_mutation_is_rejected():
    result = await schema.execute(
        """
        mutation {
            updateStock(input: {
                warehouseId: "wh-1",
                productId: "prod-1",
                quantity: 100
            }) {
                availableQuantity
            }
        }
        """,
        context_value={"db": MagicMock(), "user": None},
    )

    assert result.errors is not None
    assert "Authentication required" in result.errors[0].message
```

## DataLoader Performance Verification

A key sanity check: verify that DataLoader actually batches queries. I add a query counter to the DataLoader batch function in test mode:

```typescript
// src/__tests__/dataloader.test.ts
it('batches category loads when resolving multiple products', async () => {
  let batchCallCount = 0;

  const loader = new DataLoader(async (ids: readonly string[]) => {
    batchCallCount++;
    const categories = await prisma.category.findMany({
      where: { id: { in: ids as string[] } },
    });
    const map = new Map(categories.map((c) => [c.id, c]));
    return ids.map((id) => map.get(id)!);
  });

  // Simulate 5 concurrent requests for categories — all should batch
  await Promise.all([
    loader.load('cat-1'),
    loader.load('cat-2'),
    loader.load('cat-1'), // duplicate — returns cached result, no extra batch call
    loader.load('cat-3'),
    loader.load('cat-2'), // duplicate — cached
  ]);

  // DataLoader should have issued exactly ONE batch call for ids: [cat-1, cat-2, cat-3]
  expect(batchCallCount).toBe(1);
});
```

## Query Complexity Limiting

Without limits, a client can craft deeply nested queries that multiply into thousands of database calls. The `graphql-query-complexity` library calculates a cost score before execution and rejects queries that exceed the limit.

```bash
npm install graphql-query-complexity
```

```typescript
// src/plugins/complexity.ts
import { ApolloServerPlugin } from '@apollo/server';
import {
  fieldExtensionsEstimator,
  getComplexity,
  simpleEstimator,
} from 'graphql-query-complexity';
import { GraphQLError, separateOperations } from 'graphql';

const MAX_COMPLEXITY = 100;

export const complexityPlugin: ApolloServerPlugin = {
  async requestDidStart() {
    return {
      async didResolveOperation({ request, document, schema }) {
        const complexity = getComplexity({
          schema,
          operationName: request.operationName,
          query: document,
          variables: request.variables,
          estimators: [
            fieldExtensionsEstimator(),      // checks `complexity` extension on individual fields
            simpleEstimator({ defaultComplexity: 1 }),  // fallback: 1 per field
          ],
        });

        if (complexity > MAX_COMPLEXITY) {
          throw new GraphQLError(
            `Query complexity ${complexity} exceeds the maximum allowed complexity of ${MAX_COMPLEXITY}.`,
            { extensions: { code: 'QUERY_COMPLEXITY_EXCEEDED', complexity } },
          );
        }
      },
    };
  },
};
```

Mark expensive fields with a higher complexity cost in the schema:

```typescript
// src/resolvers/product.ts
export const productResolvers = {
  Query: {
    products: {
      resolve: async (_parent, args, ctx) => { /* ... */ },
      extensions: {
        complexity: ({ args }: { args: { first?: number } }) =>
          (args.first ?? 20) * 3, // cost scales with page size
      },
    },
  },
};
```

Register the plugin:

```typescript
const server = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    complexityPlugin,
  ],
});
```

## Persisted Queries

Persisted queries are a significant production optimisation — the client sends a hash instead of the full query document. This reduces payload size and enables server-side allowlisting (forbid ad-hoc queries in production).

Apollo Client supports automatic persisted queries (APQ) via the `createPersistedQueryLink`:

```typescript
// src/apollo.ts
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

const persistedQueriesLink = createPersistedQueryLink({ sha256 });

const httpLink = createHttpLink({ uri: 'http://localhost:4000/graphql' });

export const client = new ApolloClient({
  link: from([persistedQueriesLink, httpLink]),
  cache: new InMemoryCache(),
});
```

On the first request: the client sends the hash. The server has not seen it yet and returns a `PersistedQueryNotFound`. The client then sends the full query. On subsequent requests: only the hash is sent and the server uses the cached document.

## Deployment

### Health Checks

The TypeScript subgraph exposes Prisma connectivity as a health endpoint:

```typescript
// src/index.ts
app.get('/health', async (req, res) => {
  try {
    await prisma.$queryRaw`SELECT 1`;
    res.json({ status: 'ok', service: 'product-subgraph' });
  } catch (err) {
    res.status(503).json({ status: 'error', detail: 'Database unreachable' });
  }
});
```

The Python service already has `/health` returning `{"status": "ok"}` from Part 6.

### Environment Variables

| Variable       | Service             | Purpose                                      |
| -------------- | ------------------- | -------------------------------------------- |
| `DATABASE_URL` | TypeScript + Python | PostgreSQL connection string                 |
| `JWT_SECRET`   | TypeScript + Python | HMAC key for JWT verification                |
| `PORT`         | TypeScript          | Listening port (default 4001)                |
| `NODE_ENV`     | TypeScript          | `production` disables stack traces in errors |

### Recommended Production Checklist

* [ ] Apollo Server is started with `NODE_ENV=production` — this disables error stack traces in responses
* [ ] JWT secret is loaded from environment, not hardcoded
* [ ] Query complexity limiting is enabled
* [ ] Database connection pool size is tuned (Prisma default is 5 \* CPU cores)
* [ ] Apollo Router health check (`/health` on port 8088) is wired into the load balancer
* [ ] Supergraph schema is generated in CI (not at router startup) and committed or stored in a registry
* [ ] Apollo Studio (or a self-hosted schema registry) is configured for schema change tracking

### CI Supergraph Composition

```yaml
# .github/workflows/compose-supergraph.yml
name: Compose Supergraph
on:
  push:
    branches: [main]

jobs:
  compose:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rover
        run: curl -sSL https://rover.apollo.dev/nix/latest | sh

      - name: Compose supergraph schema
        run: |
          ~/.rover/bin/rover supergraph compose \
            --config supergraph.yaml > supergraph-schema.graphql

      - name: Upload supergraph schema
        uses: actions/upload-artifact@v4
        with:
          name: supergraph-schema
          path: supergraph-schema.graphql
```

## Series Summary

Over these seven parts, a complete GraphQL stack has been built from the ground up:

| Part                 | What Was Covered                                                                             |
| -------------------- | -------------------------------------------------------------------------------------------- |
| 1 — Introduction     | GraphQL concepts, SDL, Apollo Server 4 setup, first query execution                          |
| 2 — Schema Design    | Interfaces, unions, enums, custom scalars, pagination patterns, schema evolution             |
| 3 — Building the API | Apollo Server + Prisma, DataLoader, Zod validation, subscriptions, cursor pagination         |
| 4 — Clients          | TypeScript with code generation, Python `gql` client, subscriptions from Python              |
| 5 — Auth             | JWT context, resolver guards, schema directives, Strawberry permissions                      |
| 6 — Federation       | Apollo Federation v2, TypeScript and Python subgraphs, Apollo Router gateway                 |
| 7 — Production       | Testing strategies, DataLoader verification, query complexity, persisted queries, deployment |

The federated architecture scales well — new services can join the supergraph as independent subgraphs without touching existing services. Each team owns their part of the graph, and the router composes them transparently for clients.
