一句话总结:契约测试在消费者与提供者之间建立可验证的契约,早于部署发现问题,是微服务体系中保障 API 向后兼容的核心实践。
1. API 测试金字塔
在 GraphQL / API 工程领域,测试应遵循金字塔分层:
/\
/ \ E2E 测试(Playwright)
/ \ — 验证完整用户流程
/──────\
/ \ 契约测试(Pact / Schema Check)
/ \ — 验证消费者-提供者契约
/────────────\
/ \ 集成测试(Postman / Bruno)
/ \— 验证服务间协作
/──────────────────\
单元测试(Jest / Vitest)
— 验证 Resolver / Handler 逻辑
| 测试层级 | 数量 | 运行速度 | 反馈周期 | 成本 |
|---|---|---|---|---|
| 单元测试 | 最多 | < 100ms | 秒级 | 低 |
| 集成测试 | 中等 | 1-10s | 分钟级 | 中 |
| 契约测试 | 较少 | 1-30s | 分钟级 | 中 |
| E2E 测试 | 最少 | 10-60s | 分钟级 | 高 |
契约测试的独特价值:在消费者代码(如前端)和提供者代码(如后端)之间建立可验证的契约文件。当提供者变更导致契约破坏时,CI 在合并前即可发现问题。
2. Schema 校验
Schema 校验是最轻量的契约保证,确保服务端 Schema 符合预期的结构和类型约束。
2.1 graphql-js 基础校验
import { parse, validate } from "graphql";
import { buildSchema } from "graphql";
const schema = buildSchema(`
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
}
`);
// 校验查询文档是否合法
const query = parse(`
query GetUser {
user(id: "1") {
id
name
}
}
`);
const errors = validate(schema, query);
console.log(errors); // [] = 无错误
2.2 破坏性变更检测
# 安装 GraphQL Inspector
npm install -g @graphql-inspector/cli
# 比较两个 Schema 文件
graphql-inspector diff old-schema.graphql new-schema.graphql
# 输出示例:
# ✖ Field User.phone removed
# ✔ Enum Status.PENDING added
# ⚠ Field User.email type changed from String to String!
2.3 CI 集成
# .github/workflows/schema-check.yml
name: Schema Validation
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run generate-schema > schema.graphql
- run: npx graphql-inspector diff origin/main:schema.graphql schema.graphql
3. 消费者驱动契约测试(CDC)
3.1 CDC 核心概念
| 术语 | 说明 |
|---|---|
| Consumer | API 的消费者(前端 / 移动端 / 下游服务) |
| Provider | API 的提供者(后端服务) |
| Contract | 消费者与提供者之间的交互约定 |
| Pact | 契约文件(JSON 格式),记录期望的请求和响应 |
| Broker | 契约交换中心(Pact Broker),存储和分发契约 |
CDC 工作流程:
1. 消费者编写测试:定义期望的请求和响应
→ 生成 Pact 契约文件
→ 上传到 Pact Broker
2. 提供者获取契约:从 Broker 下载消费者的 Pact 文件
→ 执行 Provider Verification Test
→ 确认是否满足所有契约期望
3. CI 阻断:如果 Provider Verification 失败,PR 被阻断
3.2 Pact 消费者测试(前端)
// tests/pact/consumer.test.ts
import { Pact } from "@pact-foundation/pact";
import { GraphQLInteraction } from "@pact-foundation/pact";
const provider = new Pact({
consumer: "web-client",
provider: "user-service",
port: 1234,
log: "logs/pact.log",
dir: "pacts",
});
describe("User API Pact", () => {
beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
it("returns a user by id", async () => {
await provider.addInteraction(
new GraphQLInteraction()
.withOperation("GetUser")
.withQuery(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`)
.withVariables({ id: "1" })
.uponReceiving("a request for user by id")
.withRequest({ path: "/graphql", method: "POST" })
.willRespondWith({
status: 200,
body: {
data: {
user: {
id: "1",
name: "Alice",
email: "alice@example.com",
},
},
},
})
);
const client = new ApolloClient({
uri: "http://localhost:1234/graphql",
cache: new InMemoryCache(),
});
const result = await client.query({
query: GET_USER,
variables: { id: "1" },
});
expect(result.data.user.name).toBe("Alice");
});
});
3.3 Pact 提供者验证(后端)
// tests/pact/provider.test.ts
import { Verifier } from "@pact-foundation/pact";
describe("User Service Pact Verification", () => {
it("validates expectations", async () => {
await new Verifier({
provider: "user-service",
providerBaseUrl: "http://localhost:4000",
pactBrokerUrl: "https://pact-broker.example.com",
pactBrokerToken: process.env.PACT_TOKEN,
publishVerificationResult: true,
providerVersionBranch: process.env.GIT_BRANCH,
}).verifyProvider();
});
});
3.4 Pact Broker Webhook
# Broker 配置:当契约上传时,触发 Provider 验证
webhooks:
- consumer: "web-client"
events:
- contract_published
url: "https://ci.example.com/webhooks/pact"
headers:
Authorization: "Bearer ${CI_TOKEN}"
4. Postman / Bruno 集成测试
4.1 Bruno:Git 友好的 API 测试
Bruno 是 Postman 的开源替代品,所有测试用例以文件形式存储(.bru),天然支持版本控制。
# 目录结构
api-tests/
├── bruno.json # 项目配置
├── environments/
│ └── production.bru
└── graphql/
├── get-user.bru
├── create-post.bru
└── collection.bru
get-user.bru:
meta {
name: Get User
type: graphql
seq: 1
}
post {
url: {{baseUrl}}/graphql
body: graphql
auth: bearer
}
query {
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
vars {
id: "1"
}
}
tests {
test("status is 200", function() {
expect(res.status).to.equal(200);
});
test("user has ID", function() {
expect(res.body.data.user.id).to.equal("1");
});
test("response time < 200ms", function() {
expect(res.responseTime).to.be.below(200);
});
}
4.2 CLI 运行与 CI 集成
# 运行所有测试
bru run --env production
# 生成报告
bru run --env production --output results.json
# GitHub Actions
npx @usebruno/cli@latest run --env production
5. Mock Service Worker(MSW)
MSW 在浏览器/Node 中拦截网络请求,是前端测试的核心工具。
5.1 GraphQL Handler
// tests/mocks/handlers.ts
import { graphql, HttpResponse } from "msw";
export const handlers = [
graphql.query("GetUser", ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: "MSW User",
email: "msw@example.com",
},
},
});
}),
graphql.mutation("CreatePost", ({ variables }) => {
return HttpResponse.json({
data: {
createPost: {
id: "new-post-id",
title: variables.input.title,
},
},
});
}),
// 模拟网络错误
graphql.query("SearchUsers", () => {
return HttpResponse.error();
}),
];
5.2 Schema-Driven Mock
// 基于 Schema 自动生成 Mock 数据
import { addMocksToSchema } from "@graphql-tools/mock";
import { makeExecutableSchema } from "@graphql-tools/schema";
const schema = makeExecutableSchema({ typeDefs });
const mockedSchema = addMocksToSchema({ schema });
// 在 MSW 中使用
graphql.operation(({ query }) => {
const result = graphqlSync({ schema: mockedSchema, source: query });
return HttpResponse.json(result);
});
6. Playwright E2E 与 API 拦截
6.1 Mock GraphQL 请求
// tests/e2e/user-flow.spec.ts
import { test, expect } from "@playwright/test";
test("user can view profile", async ({ page }) => {
// Mock GraphQL response
await page.route("**/graphql", async (route, request) => {
if (request.postData()?.includes("GetUser")) {
await route.fulfill({
status: 200,
body: JSON.stringify({
data: {
user: {
id: "1",
name: "E2E User",
email: "e2e@example.com",
},
},
}),
});
return;
}
route.continue();
});
await page.goto("/profile");
await expect(page.locator("[data-testid='user-name']")).toHaveText("E2E User");
});
6.2 并行 API 验证
// 同时测试 UI 和 API
const [response] = await Promise.all([
page.waitForResponse("**/graphql"),
page.click("button.submit"),
]);
const json = await response.json();
expect(json.data.createPost.title).toBe("New Post");
7. CI/CD 完整流水线
7.1 GitHub Actions Workflow
# .github/workflows/api-test.yml
name: API Test Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm run typecheck
unit-test:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:unit
schema-check:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run generate-schema
- run: npx graphql-inspector diff origin/main:schema.graphql schema.graphql
integration-test:
needs: [unit-test, schema-check]
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test
contract-test-consumer:
needs: unit-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:pact:consumer
- name: Upload Pacts
uses: pact-foundation/pact-broker-publish-action@v1
with:
pact-broker-base-url: https://pact.example.com
pact-broker-token: ${{ secrets.PACT_TOKEN }}
consumer-app-version: ${{ github.sha }}
contract-test-provider:
needs: contract-test-consumer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run db:migrate # 准备测试数据库
- run: npm run test:pact:provider
env:
PACT_TOKEN: ${{ secrets.PACT_TOKEN }}
e2e-test:
needs: [integration-test, contract-test-provider]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
7.2 流水线触发策略
| 阶段 | 触发条件 | 失败阻断 |
|---|---|---|
| Lint + Type | 每次 PR | ✅ 是 |
| Schema Check | 每次 PR | ✅ 是(breaking change) |
| Unit Test | 每次 PR | ✅ 是 |
| Integration | 每次 PR | ✅ 是 |
| Contract (Consumer) | 每次 PR / push | ✅ 是 |
| Contract (Provider) | Consumer 上传后 | 通知 Provider 团队 |
| E2E | 合并到 main 后 | 通知 + issue |
8. 测试数据管理
8.1 工厂模式
// factories/user.ts
import { faker } from "@faker-js/faker";
export function createUser(overrides?: Partial<User>): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
role: "USER",
createdAt: faker.date.past(),
...overrides,
};
}
// 使用
const admin = createUser({ role: "ADMIN", name: "Test Admin" });
8.2 环境隔离
| 环境 | 数据策略 |
|---|---|
| 单元测试 | 内存数据库 / Mock |
| 集成测试 | 每次测试事务回滚(PostgreSQL BEGIN; ...; ROLLBACK;) |
| E2E 测试 | 专用测试数据库 + 种子脚本 |
| 契约测试 | 独立 Pact Mock Server |
9. 一句话总结
- Schema 校验:GraphQL Inspector + CI 校验,防止 breaking change 流入生产
- 契约测试(Pact):消费者驱动,Provider 验证,Broker 居中协调
- 集成测试(Bruno):文件化 API 测试,Git 版本控制友好
- Mock 测试(MSW):前端拦截网络请求,零后端依赖
- E2E 测试(Playwright):验证完整用户流程 + API 响应数据
- CI 流水线:lint → unit → schema → integration → contract → e2e,逐层把关
FAQ
Q1:契约测试和 E2E 测试的区别?
A:契约测试验证"接口约定"(消费者期望的 API 形状),E2E 验证"业务流程"(用户完成某功能的完整路径)。契约测试运行更快(秒级),E2E 更慢(分钟级)。两者互补——契约保证 API 兼容,E2E 保证业务正确。
Q2:Pact Broker 是否必需?
A:小团队(< 5 人)可手动共享契约文件。中型以上团队强烈建议使用 Pact Broker,它提供:契约版本管理、Provider 验证历史、Webhook 触发 CI、can-i-deploy 部署决策。
Q3:如何处理 GraphQL 查询的变体?
A:每个不同的查询形状(字段组合)都视为独立的契约。Pact 支持将操作名作为契约标识。建议使用 Persisted Queries 减少查询变体数量——白名单查询天然约束了契约范围。
Q4:MSW 能否完全替代真实后端测试?
A:MSW 适合单元/组件测试和前端开发期间 Mock。但在集成测试中仍需真实后端验证数据一致性。最佳实践:前端开发用 MSW,集成测试用 Bruno + 真实后端,E2E 用真实全栈。
Q5:Schema Check 能防止所有 breaking change 吗?
A:不能。结构型 breaking change(删除字段、改变类型)可被检测,但语义型 breaking change(字段返回数据格式变更但类型声明不变)无法通过 Schema Check 发现。需结合契约测试和 E2E 覆盖。
相关阅读
- GraphQL 基础与 Schema First 设计 — Schema 结构与 Introspection
- GraphQL 安全防护 — 输入校验与错误处理
- API 缓存与性能优化 — 性能基准与 K6 压测
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。