Testing GraphQL Federation: Subgraphs, Supergraph, and Gateway

Testing GraphQL Federation: Subgraphs, Supergraph, and Gateway

GraphQL Federation (Apollo Federation v2, Hive Federation, Cosmo) splits a GraphQL schema across multiple microservices called subgraphs. Testing federated GraphQL requires a layered approach: unit test subgraphs independently, validate schema composition, and integration-test the full supergraph.

The Three Layers of Federation Testing

  1. Subgraph tests: test each subgraph as a standalone GraphQL service
  2. Schema composition tests: verify subgraphs compose into a valid supergraph
  3. Gateway integration tests: test queries that span multiple subgraphs

Skipping layer 2 leads to runtime composition errors that are hard to debug.

Layer 1: Testing Subgraphs Independently

Each subgraph is a normal Apollo Server (or equivalent). Test it like any GraphQL service.

// users-subgraph/schema.js
const { buildSubgraphSchema } = require('@apollo/subgraph');

const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
  
  type User @key(fields: "id") {
    id: ID!
    email: String!
    name: String!
  }
  
  type Query {
    user(id: ID!): User
    me: User
  }
`;

const resolvers = {
  User: {
    __resolveReference(ref, { dataSources }) {
      return dataSources.usersAPI.getUser(ref.id);
    }
  },
  Query: {
    user: (_, { id }, { dataSources }) => dataSources.usersAPI.getUser(id),
    me: (_, __, { currentUser }) => currentUser
  }
};
// users-subgraph/test/schema.test.js
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { executeOperation } = require('@apollo/server/testing');

let server;

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

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

test('query user by ID', async () => {
  const { body } = await server.executeOperation(
    {
      query: `query GetUser($id: ID!) { user(id: $id) { id email name } }`,
      variables: { id: 'u1' }
    },
    {
      contextValue: {
        dataSources: {
          usersAPI: {
            getUser: jest.fn().mockResolvedValue({
              id: 'u1',
              email: 'alice@example.com',
              name: 'Alice'
            })
          }
        }
      }
    }
  );
  
  expect(body.kind).toBe('single');
  expect(body.singleResult.data?.user).toEqual({
    id: 'u1',
    email: 'alice@example.com',
    name: 'Alice'
  });
});

test('resolves reference for federation', async () => {
  // Test __resolveReference — called when gateway resolves cross-subgraph references
  const { body } = await server.executeOperation(
    {
      query: `
        query ($representations: [_Any!]!) {
          _entities(representations: $representations) {
            ... on User { id email }
          }
        }
      `,
      variables: {
        representations: [{ __typename: 'User', id: 'u1' }]
      }
    },
    {
      contextValue: {
        dataSources: {
          usersAPI: {
            getUser: jest.fn().mockResolvedValue({
              id: 'u1',
              email: 'alice@example.com',
              name: 'Alice'
            })
          }
        }
      }
    }
  );
  
  expect(body.singleResult.data?._entities[0].email).toBe('alice@example.com');
});

The _entities query test is critical — it's how the gateway fetches related entities across subgraph boundaries.

Layer 2: Schema Composition Tests

Schema composition can fail in ways that aren't caught at runtime until a query hits the failing path. Use @apollo/composition to validate composition in CI.

// test/composition.test.js
const { composeServices } = require('@apollo/composition');
const { parse } = require('graphql');

const usersSubgraph = {
  name: 'users',
  typeDefs: parse(fs.readFileSync('./users-subgraph/schema.graphql', 'utf8'))
};

const ordersSubgraph = {
  name: 'orders',
  typeDefs: parse(fs.readFileSync('./orders-subgraph/schema.graphql', 'utf8'))
};

const productsSubgraph = {
  name: 'products',
  typeDefs: parse(fs.readFileSync('./products-subgraph/schema.graphql', 'utf8'))
};

test('all subgraphs compose without errors', () => {
  const result = composeServices([usersSubgraph, ordersSubgraph, productsSubgraph]);
  
  if (result.errors?.length > 0) {
    const errorMessages = result.errors.map(e => e.message).join('\n');
    throw new Error(`Composition failed:\n${errorMessages}`);
  }
  
  expect(result.supergraphSdl).toBeTruthy();
});

test('composed schema includes all expected types', () => {
  const { supergraphSdl } = composeServices([usersSubgraph, ordersSubgraph, productsSubgraph]);
  
  expect(supergraphSdl).toContain('type User');
  expect(supergraphSdl).toContain('type Order');
  expect(supergraphSdl).toContain('type Product');
});

test('User type is owned by users subgraph', () => {
  const result = composeServices([usersSubgraph, ordersSubgraph, productsSubgraph]);
  
  // No errors means the @key directives are consistent
  expect(result.errors).toBeUndefined();
});

Run this test every time a subgraph schema changes. Composition failures are caught before deployment.

Layer 3: Gateway Integration Tests

Integration tests verify that cross-subgraph queries work end-to-end.

// test/integration/gateway.test.js
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
const { ApolloServer } = require('@apollo/server');

let gateway;
let server;

beforeAll(async () => {
  // Use locally running subgraphs for integration tests
  gateway = new ApolloGateway({
    supergraphSdl: new IntrospectAndCompose({
      subgraphs: [
        { name: 'users', url: 'http://localhost:4001/graphql' },
        { name: 'orders', url: 'http://localhost:4002/graphql' },
        { name: 'products', url: 'http://localhost:4003/graphql' }
      ]
    })
  });
  
  server = new ApolloServer({ gateway });
  await server.start();
});

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

test('cross-subgraph query: user with orders', async () => {
  // Orders subgraph extends User type with orders field
  const { body } = await server.executeOperation({
    query: `
      query UserWithOrders($userId: ID!) {
        user(id: $userId) {
          id
          name
          orders {
            id
            total
            status
          }
        }
      }
    `,
    variables: { userId: 'u1' }
  });
  
  expect(body.singleResult.errors).toBeUndefined();
  expect(body.singleResult.data?.user.orders).toBeInstanceOf(Array);
});

Integration tests require running subgraphs. In CI, start them with docker-compose or use jest-globalSetup to start processes before the test suite.

Testing Schema Composition Breakage

Write tests that catch common composition mistakes before they reach production.

// These should fail composition — test that you catch them

test('fails composition when two subgraphs own the same field', () => {
  const conflictingUsers = {
    name: 'users2',
    typeDefs: parse(`
      extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
      type User @key(fields: "id") {
        id: ID!
        email: String!  # also defined in users subgraph — conflict
      }
    `)
  };
  
  const result = composeServices([usersSubgraph, conflictingUsers]);
  
  expect(result.errors?.length).toBeGreaterThan(0);
  expect(result.errors[0].message).toMatch(/field.*conflict/i);
});

test('fails composition when @key references a nonexistent field', () => {
  const badSubgraph = {
    name: 'bad',
    typeDefs: parse(`
      extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
      type Product @key(fields: "nonExistentField") {
        id: ID!
      }
    `)
  };
  
  const result = composeServices([badSubgraph]);
  
  expect(result.errors?.length).toBeGreaterThan(0);
});

Snapshot Testing for the Supergraph SDL

Lock your supergraph SDL in a snapshot to catch unintended schema changes.

test('supergraph SDL matches snapshot', () => {
  const { supergraphSdl } = composeServices([usersSubgraph, ordersSubgraph, productsSubgraph]);
  
  expect(supergraphSdl).toMatchSnapshot();
});

When you intentionally change a subgraph schema, update the snapshot with jest --updateSnapshot. Any unintended SDL changes will fail this test.

CI Pipeline for Federation Testing

# .github/workflows/graphql-federation.yml
jobs:
  test-subgraphs:
    strategy:
      matrix:
        subgraph: [users, orders, products]
    steps:
      - run: cd ${{ matrix.subgraph }}-subgraph && npm test

  test-composition:
    needs: test-subgraphs
    steps:
      - run: npm run test:composition

  test-integration:
    needs: test-composition
    services:
      users-subgraph:
        image: my-registry/users-subgraph:latest
      orders-subgraph:
        image: my-registry/orders-subgraph:latest
    steps:
      - run: npm run test:integration

Run subgraph tests in parallel. Only run integration tests if composition passes. This order prevents slow integration test runs when a schema is already broken at the composition level.

Read more

Start now free