乐闻世界logo
搜索文章和话题

GraphQL 测试有哪些策略和最佳实践

2月21日 17:00

GraphQL 测试策略与最佳实践

GraphQL API 的测试对于确保代码质量和系统稳定性至关重要。以下是 GraphQL 测试的全面策略和最佳实践。

1. 测试类型

单元测试

测试单个 Resolver 函数的逻辑。

javascript
import { userResolvers } from './user.resolver'; describe('User Resolvers', () => { describe('Query.user', () => { it('should return user by id', async () => { const mockUser = { id: '1', name: 'John', email: 'john@example.com' }; User.findById = jest.fn().mockResolvedValue(mockUser); const result = await userResolvers.Query.user(null, { id: '1' }); expect(result).toEqual(mockUser); expect(User.findById).toHaveBeenCalledWith('1'); }); it('should throw error if user not found', async () => { User.findById = jest.fn().mockResolvedValue(null); await expect( userResolvers.Query.user(null, { id: '1' }) ).rejects.toThrow('User not found'); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const input = { name: 'John Doe', email: 'john@example.com' }; const createdUser = { id: '1', ...input }; User.create = jest.fn().mockResolvedValue(createdUser); const result = await userResolvers.Mutation.createUser( null, { input } ); expect(result).toEqual(createdUser); expect(User.create).toHaveBeenCalledWith(input); }); }); });

集成测试

测试多个组件协同工作。

javascript
import { ApolloServer } from 'apollo-server'; import { createTestClient } from 'apollo-server-testing'; import { typeDefs, resolvers } from './schema'; describe('GraphQL API Integration Tests', () => { const server = new ApolloServer({ typeDefs, resolvers }); const { query, mutate } = createTestClient(server); describe('Query.users', () => { it('should return all users', async () => { const GET_USERS = ` query GetUsers { users { id name email } } `; const { data, errors } = await query(GET_USERS); expect(errors).toBeUndefined(); expect(data.users).toBeDefined(); expect(Array.isArray(data.users)).toBe(true); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const CREATE_USER = ` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; const variables = { input: { name: 'John Doe', email: 'john@example.com' } }; const { data, errors } = await mutate(CREATE_USER, { variables }); expect(errors).toBeUndefined(); expect(data.createUser).toBeDefined(); expect(data.createUser.name).toBe('John Doe'); }); }); });

E2E 测试

测试完整的用户流程。

javascript
import { createHttpLink } from 'apollo-link-http'; import { ApolloClient } from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; import fetch from 'node-fetch'; describe('E2E Tests', () => { let client; beforeAll(() => { client = new ApolloClient({ link: createHttpLink({ uri: 'http://localhost:4000/graphql', fetch }), cache: new InMemoryCache() }); }); it('should complete user registration and login flow', async () => { // 注册用户 const REGISTER = ` mutation Register($input: RegisterInput!) { register(input: $input) { id name email } } `; const registerResult = await client.mutate({ mutation: REGISTER, variables: { input: { name: 'John Doe', email: 'john@example.com', password: 'password123' } } }); expect(registerResult.data.register).toBeDefined(); // 登录用户 const LOGIN = ` mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { token user { id name } } } `; const loginResult = await client.mutate({ mutation: LOGIN, variables: { email: 'john@example.com', password: 'password123' } }); expect(loginResult.data.login.token).toBeDefined(); expect(loginResult.data.login.user.name).toBe('John Doe'); }); });

2. 测试工具

Jest

javascript
// jest.config.js module.exports = { testEnvironment: 'node', setupFilesAfterEnv: ['<rootDir>/tests/setup.js'], testMatch: ['**/tests/**/*.test.js'], collectCoverageFrom: [ 'src/**/*.js', '!src/**/*.test.js' ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } };

Mocha + Chai

javascript
const { expect } = require('chai'); const { ApolloServer } = require('apollo-server'); const { createTestClient } = require('apollo-server-testing'); describe('GraphQL API Tests', () => { let server; let client; beforeEach(() => { server = new ApolloServer({ typeDefs, resolvers }); client = createTestClient(server); }); it('should return user', async () => { const { data } = await client.query({ query: 'query { user(id: "1") { id name } }' }); expect(data.user).to.exist; expect(data.user.name).to.be.a('string'); }); });

3. Mock 数据

使用 Mock 数据库

javascript
const mockDatabase = { users: [ { id: '1', name: 'John', email: 'john@example.com' }, { id: '2', name: 'Jane', email: 'jane@example.com' } ], posts: [ { id: '1', title: 'Post 1', authorId: '1' }, { id: '2', title: 'Post 2', authorId: '2' } ] }; const mockUserModel = { findById: (id) => { return Promise.resolve( mockDatabase.users.find(user => user.id === id) ); }, findAll: () => { return Promise.resolve(mockDatabase.users); }, create: (data) => { const newUser = { id: String(mockDatabase.users.length + 1), ...data }; mockDatabase.users.push(newUser); return Promise.resolve(newUser); } }; // 在测试中使用 jest.mock('../models/User', () => mockUserModel);

使用 Faker.js 生成测试数据

javascript
const faker = require('faker'); function generateMockUser() { return { id: faker.random.uuid(), name: `${faker.name.firstName()} ${faker.name.lastName()}`, email: faker.internet.email(), age: faker.datatype.number({ min: 18, max: 80 }) }; } function generateMockUsers(count = 10) { return Array.from({ length: count }, generateMockUser); } describe('User Tests', () => { it('should handle multiple users', async () => { const mockUsers = generateMockUsers(5); User.findAll = jest.fn().mockResolvedValue(mockUsers); const result = await resolvers.Query.users(); expect(result).toHaveLength(5); expect(result[0].email).toMatch(/@/); }); });

4. 测试 Context

测试认证 Context

javascript
describe('Authenticated Resolvers', () => { it('should return current user', async () => { const mockUser = { id: '1', name: 'John' }; const context = { user: mockUser }; const result = await resolvers.Query.me(null, {}, context); expect(result).toEqual(mockUser); }); it('should throw error if not authenticated', async () => { const context = { user: null }; await expect( resolvers.Query.me(null, {}, context) ).rejects.toThrow('Authentication required'); }); });

测试数据源 Context

javascript
describe('Data Source Tests', () => { it('should use data source', async () => { const mockDataSource = { getUser: jest.fn().mockResolvedValue({ id: '1', name: 'John' }) }; const context = { dataSources: { userAPI: mockDataSource } }; await resolvers.Query.user(null, { id: '1' }, context); expect(mockDataSource.getUser).toHaveBeenCalledWith('1'); }); });

5. 测试错误处理

测试验证错误

javascript
describe('Validation Tests', () => { it('should validate email format', async () => { const input = { name: 'John', email: 'invalid-email' }; await expect( resolvers.Mutation.createUser(null, { input }) ).rejects.toThrow('Invalid email format'); }); it('should require required fields', async () => { const input = { name: 'John' // email is missing }; await expect( resolvers.Mutation.createUser(null, { input }) ).rejects.toThrow('Email is required'); }); });

测试 GraphQL 错误

javascript
describe('Error Handling Tests', () => { it('should return GraphQL error for not found', async () => { User.findById = jest.fn().mockResolvedValue(null); const { data, errors } = await client.query({ query: 'query { user(id: "999") { id name } }' }); expect(data.user).toBeNull(); expect(errors).toBeDefined(); expect(errors[0].message).toContain('not found'); }); });

6. 测试订阅

测试订阅 Resolver

javascript
describe('Subscription Tests', () => { it('should publish post created event', async () => { const mockPost = { id: '1', title: 'New Post' }; const mockIterator = { [Symbol.asyncIterator]: jest.fn().mockReturnValue( (async function* () { yield { postCreated: mockPost }; })() ) }; pubsub.asyncIterator = jest.fn().mockReturnValue(mockIterator); const iterator = resolvers.Subscription.postCreated.subscribe(); const result = await iterator.next(); expect(result.value.postCreated).toEqual(mockPost); }); });

测试订阅过滤

javascript
describe('Subscription Filtering Tests', () => { it('should filter subscriptions by userId', async () => { const mockIterator = { [Symbol.asyncIterator]: jest.fn().mockReturnValue( (async function* () { yield { notification: { userId: '1', message: 'Hello' } }; yield { notification: { userId: '2', message: 'Hi' } }; })() ) }; pubsub.asyncIterator = jest.fn().mockReturnValue(mockIterator); const iterator = resolvers.Subscription.notification.subscribe( null, { userId: '1' } ); const results = []; for await (const event of iterator) { results.push(event); if (results.length >= 2) break; } expect(results).toHaveLength(1); expect(results[0].notification.userId).toBe('1'); }); });

7. 性能测试

测试查询性能

javascript
describe('Performance Tests', () => { it('should handle large datasets efficiently', async () => { const largeDataset = generateMockUsers(10000); User.findAll = jest.fn().mockResolvedValue(largeDataset); const startTime = Date.now(); const result = await resolvers.Query.users(); const duration = Date.now() - startTime; expect(result).toHaveLength(10000); expect(duration).toBeLessThan(1000); // 应该在 1 秒内完成 }); });

测试 N+1 查询

javascript
describe('N+1 Query Tests', () => { it('should not have N+1 query problem', async () => { const posts = generateMockPosts(10); const users = generateMockUsers(10); Post.findAll = jest.fn().mockResolvedValue(posts); User.findById = jest.fn() .mockImplementation((id) => Promise.resolve(users.find(u => u.id === id)) ); const result = await resolvers.Query.posts(); // 应该只调用一次批量查询,而不是 10 次单独查询 expect(User.findById).not.toHaveBeenCalledTimes(10); }); });

8. 测试覆盖率

配置覆盖率

javascript
// package.json { "scripts": { "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch" } }

生成覆盖率报告

bash
npm run test:coverage

9. 测试最佳实践

实践说明
测试所有 Resolvers确保每个 Resolver 都有测试
使用 Mock 数据隔离测试,避免依赖外部服务
测试错误场景验证错误处理逻辑
测试边界条件测试空值、极大值等
保持测试独立每个测试应该独立运行
使用描述性名称测试名称应该清楚描述测试内容
测试 Context验证认证、授权等 Context 功能
测试订阅确保订阅功能正常工作
性能测试确保查询性能符合预期
监控覆盖率保持高测试覆盖率

10. 常见测试问题及解决方案

问题原因解决方案
测试不稳定异步操作未正确处理使用 async/await、适当的等待
Mock 失败Mock 配置不正确检查 Mock 的配置和调用
测试慢测试数据量过大使用较小的测试数据集
覆盖率低未测试所有代码路径增加测试用例,覆盖所有分支
测试难以维护测试代码复杂重构测试代码,使用测试工具
标签:GraphQL