@sourceallies/apollo-schema-mocks
v1.2.1
Published
Schema-aware GraphQL mocking utilities for Apollo Client + Vitest testing
Readme
@sourceallies/apollo-schema-mocks
Schema-aware GraphQL mocking utilities for Apollo Client + Vitest testing.
Features
- Schema-based mocking - Uses your actual GraphQL schema for type-safe mocks
- Spy/mock separation - Track calls separately from return values (prevents nested mock issues)
- Sequential responses -
mockReturnValueOnce()support for refetch/pagination testing - Scalar success values -
scalarSuccess()for queries that return scalar types (String!, ID!, etc.) - Conditional mocks -
createConditionalResolver()for variable-dependent behavior - Flexible schema input - Accepts IntrospectionQuery, SDL string, or GraphQLSchema
- Schema caching - Improves test performance with WeakMap-based caching
- Error mock helpers - Pre-built helpers for common error scenarios
- Framework-agnostic - Works with React, Vue, Angular, Svelte, or any Apollo Client usage
Installation
npm install --save-dev @sourceallies/apollo-schema-mocksPeer Dependencies
@apollo/client^3.0.0graphql^16.0.0vitest>=1.0.0 (optional, required for spy mocks)
Quick Start
Basic Usage
import { createMockApolloClientWithSchema } from '@sourceallies/apollo-schema-mocks';
import introspectionResult from './schema.json';
// Create a mock Apollo Client
const { client, schema } = createMockApolloClientWithSchema({
schema: introspectionResult,
queryMocks: {
getUser: () => ({ id: '1', name: 'Alice', email: '[email protected]' }),
getUsers: () => [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }],
},
mutationMocks: {
createUser: () => ({ id: 'new-1', name: 'NewUser' }),
},
});
// Use the client in your tests
const result = await client.query({ query: GET_USER, variables: { id: '1' } });Using Spy Mocks for Assertions
Spy mocks let you verify how operations were called while returning mock data:
import {
createMockApolloClientWithSchema,
createQuerySpyMocks,
createMutationSpyMocks,
combineFieldMocks,
} from '@sourceallies/apollo-schema-mocks';
// Create spy mocks - spies are separate from mocks
const { spies: querySpies, mocks: queryMocks } = createQuerySpyMocks({
getUser: { id: '1', name: 'Alice' },
getUsers: [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }],
});
const { spies: mutationSpies, mocks: mutationMocks } = createMutationSpyMocks({
createUser: { id: 'new-1', name: 'NewUser' },
});
// Combine query and mutation mocks
const combined = combineFieldMocks(
queryMocks,
mutationMocks,
);
// Create the mock client
const { client } = createMockApolloClientWithSchema({
schema: introspectionResult,
queryMocks: combined.Query,
mutationMocks: combined.Mutation,
});
// Run your test
await client.query({ query: GET_USER, variables: { id: '1' } });
await client.mutate({ mutation: CREATE_USER, variables: { input: { name: 'Alice' } } });
// Assert on spies
expect(querySpies.getUser).toHaveBeenCalledWith({ id: '1' });
expect(mutationSpies.createUser).toHaveBeenCalledWith({ input: { name: 'Alice' } });Sequential Responses with mockReturnValueOnce
Spy mocks support mockReturnValueOnce() for testing refetch, pagination, and optimistic update scenarios:
const { spies, mocks, mockData } = createQuerySpyMocks({
getOrders: { orders: [order1, order2], totalCount: 2 },
});
// Override the first call to return different data
spies.getOrders.mockReturnValueOnce({ orders: [order1], totalCount: 1 });
// First call returns the override (1 order)
// Subsequent calls return the default mockData (2 orders)Scalar Success Values
When mocking queries that return scalar types (String!, ID!, Int!), use scalarSuccess() to distinguish from error strings:
import { createQuerySpyMocks, scalarSuccess } from '@sourceallies/apollo-schema-mocks';
const { mocks } = createQuerySpyMocks({
getEmailBody: scalarSuccess('Hello, this is the email body'),
getUserCount: scalarSuccess(42),
isFeatureEnabled: scalarSuccess(true),
});Without scalarSuccess(), a plain string like 'Hello' would be treated as an error message.
Conditional Mocks
For mocks that need to return different results based on variables at call time:
import { createConditionalResolver } from '@sourceallies/apollo-schema-mocks';
// Use directly as a query/mutation mock
const { client } = createMockApolloClientWithSchema({
schema: introspectionResult,
queryMocks: {
getUser: createConditionalResolver(
(vars) => (vars as { id: string }).id === '123',
{ id: '123', name: 'Alice' },
'User not found',
),
},
});Error Mocks
Test error handling with pre-built error helpers:
import {
createQuerySpyMocks,
createErrorMock,
createAuthenticationError,
createAuthorizationError,
createNotFoundError,
createValidationError,
} from '@sourceallies/apollo-schema-mocks';
const { mocks } = createQuerySpyMocks({
// Custom error
getUser: createErrorMock('User not found', { extensions: { code: 'NOT_FOUND' } }),
// Pre-built error helpers
getSecretData: createAuthenticationError(),
getAdminData: createAuthorizationError(),
getProfile: createNotFoundError('Profile', '123'),
});
const { mocks: mutationMocks } = createMutationSpyMocks({
createUser: createValidationError('Email is required', ['input', 'email']),
});
// Simple string errors
const { mocks: errorMocks } = createQuerySpyMocks({
getUser: 'User not found', // Throws GraphQLError with this message
});Available error helpers:
createErrorMock(message, options)- Custom error with extensionscreateMultipleErrorsMock(errors)- Multiple errors in one responsecreateAuthenticationError(message?)- UNAUTHENTICATED errorcreateAuthorizationError(message?)- FORBIDDEN errorcreateValidationError(message, field?)- VALIDATION_ERROR errorcreateNotFoundError(resource, id?)- NOT_FOUND errorcreateInternalError(message?)- INTERNAL_SERVER_ERROR errorcreateNetworkError(message?)- NETWORK_ERROR errorconditionalErrorMock(condition, error, data)- Return error or data based on conditioncreateConditionalResolver(condition, data, error)- Variable-dependent success/error at call time
Schema Caching
The library automatically caches built schemas for performance:
import { schemaCacheStats, clearSchemaCache } from '@sourceallies/apollo-schema-mocks';
// Check cache performance
console.log(schemaCacheStats.hitRate); // e.g., 0.95
console.log(schemaCacheStats.hits); // e.g., 19
console.log(schemaCacheStats.misses); // e.g., 1
// Reset stats between test suites if needed
schemaCacheStats.reset();
// Clear cache if needed (rarely necessary)
clearSchemaCache();Custom Scalar Mocks
Override the default scalar mocks or add your own:
const { client } = createMockApolloClientWithSchema({
schema: introspectionResult,
scalars: {
Date: () => '2024-01-15',
DateTime: () => '2024-01-15T10:30:00Z',
JSON: () => ({ custom: 'data' }),
CustomScalar: () => 'custom-value',
},
queryMocks: {
// ...
},
});Default scalar mocks:
String-"mock-string"Int-42Float-3.14Boolean-trueID-"mock-id"Date-"2024-01-01"DateTime-"2024-01-01T00:00:00Z"JSON- Empty objectBigInt- Max safe integer as string
API Reference
Core
| Function | Description |
|----------|-------------|
| createMockApolloClientWithSchema(options) | Creates a mock Apollo Client with schema-based mocks |
| getOrBuildSchema(input) | Builds or retrieves a cached GraphQL schema |
| clearSchemaCache() | Clears the schema cache |
| schemaCacheStats | Object with cache hit/miss statistics |
Helpers
| Function | Description |
|----------|-------------|
| createQuerySpyMocks(mocks) | Creates query mocks with spies for assertions |
| createQueryMocks(mocks) | Creates simple query mocks without spies |
| createQueryMock(name, value) | Creates a single query mock |
| createMutationSpyMocks(mocks) | Creates mutation mocks with spies |
| createMutationMocks(mocks) | Creates simple mutation mocks |
| createMutationMock(name, value) | Creates a single mutation mock |
| createSubscriptionSpyMocks(mocks) | Creates subscription mocks with spies |
| createSubscriptionMocks(mocks) | Creates simple subscription mocks |
| createSubscriptionMock(name, value) | Creates a single subscription mock |
| combineFieldMocks(...results) | Combines multiple mock results |
| scalarSuccess(value) | Wraps a scalar value for successful mock returns |
| isScalarSuccess(value) | Type guard for scalar success values |
| isErrorMock(value) | Type guard for error mocks |
| createConditionalResolver(condition, data, error) | Variable-dependent mock |
Schema Input Formats
The library accepts schemas in multiple formats:
// 1. IntrospectionQuery (from graphql-codegen or similar)
import introspection from './schema.json';
createMockApolloClientWithSchema({ schema: introspection, ... });
// 2. SDL string
const sdl = `
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
}
`;
createMockApolloClientWithSchema({ schema: sdl, ... });
// 3. GraphQLSchema object
import { buildSchema } from 'graphql';
const schema = buildSchema(sdl);
createMockApolloClientWithSchema({ schema, ... });Why Spy/Mock Separation?
Traditional mocking approaches often use vi.fn().mockReturnValue() directly as resolvers. This causes issues with nested mock values because Vitest's mock functions have additional properties that interfere with GraphQL field resolution.
This library separates concerns:
- Spies are Vitest mock functions for tracking calls, with
mockImplementationreturning the mock data - Mocks are plain functions that delegate to the spy
// WRONG - causes nested mock value issues
const userMock = vi.fn().mockReturnValue({ id: '1', name: 'Alice' });
// userMock has extra properties that confuse GraphQL
// CORRECT - this library's approach
const { spies, mocks } = createQuerySpyMocks({
user: { id: '1', name: 'Alice' },
});
// spies.user is for assertions (default implementation returns mock data)
// mocks.Query.user delegates to the spy and returns clean data
// spies.user.mockReturnValueOnce() can override responses for one callDocumentation
Requirements
- Node.js >= 18
- Apollo Client >= 3.0.0
- GraphQL >= 16.0.0
License
MIT - See LICENSE for details.
