测试金字塔
测试金字塔:
┌─────────┐
│ E2E │ 少量、慢、贵
─┤ Tests ├─
/ └─────────┘ \
/ \
┌─────────────────┐
│ Integration │ 适量、中等
─┤ Tests ├─
/ └─────────────────┘ \
/ \
┌─────────────────────────┐
│ Unit Tests │ 大量、快、便宜
└─────────────────────────┘
比例建议:
- 单元测试:70%(快速反馈,覆盖核心逻辑)
- 集成测试:20%(验证组件协作)
- E2E测试:10%(验证关键用户流程)
测试环境隔离:Testcontainers 与内存数据库
生产级测试必须保证测试之间相互隔离、不依赖外部系统状态。Testcontainers 通过在 Docker 中启动真实的数据库、Redis、Kafka 等依赖,为每个测试提供干净的环境。
// tests/setup/testcontainers.js
const { GenericContainer } = require('testcontainers');
let postgresContainer;
let redisContainer;
beforeAll(async () => {
// 启动 PostgreSQL 容器
postgresContainer = await new GenericContainer('postgres:15')
.withExposedPorts(5432)
.withEnvironment({
POSTGRES_USER: 'test',
POSTGRES_PASSWORD: 'test',
POSTGRES_DB: 'testdb',
})
.start();
// 启动 Redis 容器
redisContainer = await new GenericContainer('redis:7')
.withExposedPorts(6379)
.start();
process.env.DATABASE_URL = `postgresql://test:test@localhost:${postgresContainer.getMappedPort(5432)}/testdb`;
process.env.REDIS_URL = `redis://localhost:${redisContainer.getMappedPort(6379)}`;
}, 120000); // 2 分钟超时
afterAll(async () => {
await postgresContainer?.stop();
await redisContainer?.stop();
});
Testcontainers vs 内存数据库 vs 真实环境:
| 方案 | 代表 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 内存数据库 | sqlite, H2 | 启动极快 (<100ms) | 与生产数据库行为不一致 | 简单 CRUD 单元测试 |
| Testcontainers | PostgreSQL, MySQL | 与生产完全一致 | 启动较慢 (5-15s) | 集成测试、复杂查询验证 |
| 真实环境 | 共享 Dev DB | 无容器开销 | 测试间互相干扰 | 团队规模 < 5 |
最佳实践:CI 流水线中使用 Testcontainers,本地开发使用
[docker](/posts/docker/)-compose启动共享依赖。引入jest --detectOpenHandles检测未关闭的数据库连接,避免测试挂起。
单元测试
服务层测试
// services/orderService.js
class OrderService {
constructor(orderRepo, inventoryClient, paymentClient) {
this.orderRepo = orderRepo;
this.inventoryClient = inventoryClient;
this.paymentClient = paymentClient;
}
async createOrder(userId, items, shippingAddress) {
// 验证库存
const stockCheck = await this.inventoryClient.checkStock(items);
if (!stockCheck.available) {
throw new InsufficientStockError(stockCheck.details);
}
// 计算总价
const totalAmount = this.calculateTotal(items);
// 创建订单
const order = {
id: generateId(),
userId,
items,
shippingAddress,
totalAmount,
status: 'PENDING',
createdAt: new Date()
};
await this.orderRepo.save(order);
return order;
}
calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
async cancelOrder(orderId, reason) {
const order = await this.orderRepo.findById(orderId);
if (!order) {
throw new OrderNotFoundError(orderId);
}
if (order.status === 'SHIPPED') {
throw new OrderAlreadyShippedError(orderId);
}
order.status = 'CANCELLED';
order.cancelledAt = new Date();
order.cancelReason = reason;
await this.orderRepo.update(order);
// 释放库存
await this.inventoryClient.releaseStock(orderId);
return order;
}
}
// tests/orderService.test.js
const OrderService = require('../services/orderService');
const { InsufficientStockError, OrderNotFoundError } = require('../errors');
describe('OrderService', () => {
let orderService;
let mockOrderRepo;
let mockInventoryClient;
let mockPaymentClient;
beforeEach(() => {
mockOrderRepo = {
save: jest.fn(),
findById: jest.fn(),
update: jest.fn()
};
mockInventoryClient = {
checkStock: jest.fn(),
releaseStock: jest.fn()
};
mockPaymentClient = {
processPayment: jest.fn()
};
orderService = new OrderService(
mockOrderRepo,
mockInventoryClient,
mockPaymentClient
);
});
describe('createOrder', () => {
it('should create order when stock is available', async () => {
const items = [
{ productId: 'p1', quantity: 2, price: 50 },
{ productId: 'p2', quantity: 1, price: 100 }
];
mockInventoryClient.checkStock.mockResolvedValue({
available: true
});
mockOrderRepo.save.mockResolvedValue();
const order = await orderService.createOrder(
'user123',
items,
{ street: '123 Main St' }
);
expect(order.userId).toBe('user123');
expect(order.items).toEqual(items);
expect(order.totalAmount).toBe(200); // 2*50 + 1*100
expect(order.status).toBe('PENDING');
expect(mockOrderRepo.save).toHaveBeenCalledWith(order);
});
it('should throw InsufficientStockError when stock is unavailable', async () => {
const items = [{ productId: 'p1', quantity: 10 }];
mockInventoryClient.checkStock.mockResolvedValue({
available: false,
details: [{ productId: 'p1', requested: 10, available: 5 }]
});
await expect(orderService.createOrder('user123', items, {}))
.rejects
.toThrow(InsufficientStockError);
expect(mockOrderRepo.save).not.toHaveBeenCalled();
});
});
describe('calculateTotal', () => {
it('should calculate total correctly', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 20, quantity: 3 },
{ price: 5, quantity: 1 }
];
const total = orderService.calculateTotal(items);
expect(total).toBe(85); // 20 + 60 + 5
});
it('should return 0 for empty items', () => {
expect(orderService.calculateTotal([])).toBe(0);
});
});
describe('cancelOrder', () => {
it('should cancel order and release stock', async () => {
const order = {
id: 'order123',
status: 'PENDING',
items: [{ productId: 'p1', quantity: 2 }]
};
mockOrderRepo.findById.mockResolvedValue(order);
mockOrderRepo.update.mockResolvedValue();
mockInventoryClient.releaseStock.mockResolvedValue();
const cancelledOrder = await orderService.cancelOrder(
'order123',
'Customer request'
);
expect(cancelledOrder.status).toBe('CANCELLED');
expect(cancelledOrder.cancelReason).toBe('Customer request');
expect(mockOrderRepo.update).toHaveBeenCalledWith(cancelledOrder);
expect(mockInventoryClient.releaseStock).toHaveBeenCalledWith('order123');
});
it('should throw OrderNotFoundError when order does not exist', async () => {
mockOrderRepo.findById.mockResolvedValue(null);
await expect(orderService.cancelOrder('nonexistent', 'reason'))
.rejects
.toThrow(OrderNotFoundError);
});
it('should throw error when order already shipped', async () => {
const order = {
id: 'order123',
status: 'SHIPPED'
};
mockOrderRepo.findById.mockResolvedValue(order);
await expect(orderService.cancelOrder('order123', 'reason'))
.rejects
.toThrow('Order already shipped');
});
});
});
集成测试
API端点测试
// tests/api/orders.test.js
const request = require('supertest');
const app = require('../../app');
const { setupTestDB, teardownTestDB, clearDB } = require('../helpers/db');
const { generateToken } = require('../helpers/auth');
const Order = require('../../models/Order');
const nock = require('nock');
describe('Orders API', () => {
beforeAll(async () => {
await setupTestDB();
});
afterAll(async () => {
await teardownTestDB();
});
beforeEach(async () => {
await clearDB();
});
afterEach(() => {
nock.cleanAll();
});
describe('POST /api/orders', () => {
it('should create order successfully', async () => {
const token = generateToken({ userId: 'user123' });
// Mock库存服务
nock(process.env.INVENTORY_SERVICE_URL)
.post('/api/stock/check')
.reply(200, { available: true });
const response = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({
items: [
{ productId: 'p1', quantity: 2, price: 50 }
],
shippingAddress: {
street: '123 Main St',
city: 'New York'
}
})
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
userId: 'user123',
status: 'PENDING',
totalAmount: 100
});
// 验证数据库
const order = await Order.findById(response.body.id);
expect(order).toBeTruthy();
expect(order.userId).toBe('user123');
});
it('should return 401 without authentication', async () => {
await request(app)
.post('/api/orders')
.send({ items: [] })
.expect(401);
});
it('should return 400 with invalid items', async () => {
const token = generateToken({ userId: 'user123' });
const response = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({
items: [], // 空数组
shippingAddress: {}
})
.expect(400);
expect(response.body.error).toContain('items');
});
it('should return 400 when stock is insufficient', async () => {
const token = generateToken({ userId: 'user123' });
nock(process.env.INVENTORY_SERVICE_URL)
.post('/api/stock/check')
.reply(200, {
available: false,
details: [{ productId: 'p1', requested: 10, available: 5 }]
});
const response = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({
items: [{ productId: 'p1', quantity: 10, price: 50 }]
})
.expect(400);
expect(response.body.error).toBe('Insufficient stock');
});
it('should handle inventory service timeout', async () => {
const token = generateToken({ userId: 'user123' });
nock(process.env.INVENTORY_SERVICE_URL)
.post('/api/stock/check')
.delay(5000)
.reply(200, { available: true });
await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({
items: [{ productId: 'p1', quantity: 1, price: 50 }]
})
.expect(503); // Service unavailable
});
});
describe('GET /api/orders/:id', () => {
it('should return order by id', async () => {
const token = generateToken({ userId: 'user123' });
const order = await Order.create({
userId: 'user123',
items: [{ productId: 'p1', quantity: 1, price: 100 }],
totalAmount: 100,
status: 'PENDING'
});
const response = await request(app)
.get(`/api/orders/${order.id}`)
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.id).toBe(order.id);
expect(response.body.userId).toBe('user123');
});
it('should return 404 for non-existent order', async () => {
const token = generateToken({ userId: 'user123' });
await request(app)
.get('/api/orders/nonexistent')
.set('Authorization', `Bearer ${token}`)
.expect(404);
});
it('should return 403 when accessing other user order', async () => {
const token = generateToken({ userId: 'user123' });
const order = await Order.create({
userId: 'user456', // 其他用户
items: [],
totalAmount: 0,
status: 'PENDING'
});
await request(app)
.get(`/api/orders/${order.id}`)
.set('Authorization', `Bearer ${token}`)
.expect(403);
});
});
});
数据库测试
// tests/repositories/orderRepository.test.js
const OrderRepository = require('../../repositories/orderRepository');
const { setupTestDB, teardownTestDB, clearDB } = require('../helpers/db');
describe('OrderRepository', () => {
let repo;
beforeAll(async () => {
await setupTestDB();
repo = new OrderRepository();
});
afterAll(async () => {
await teardownTestDB();
});
beforeEach(async () => {
await clearDB();
});
describe('save', () => {
it('should save order to database', async () => {
const order = {
id: 'order123',
userId: 'user123',
items: [{ productId: 'p1', quantity: 2 }],
totalAmount: 100,
status: 'PENDING'
};
await repo.save(order);
const saved = await repo.findById('order123');
expect(saved).toMatchObject(order);
});
});
describe('findByUserId', () => {
it('should return orders for specific user', async () => {
await repo.save({
id: 'order1',
userId: 'user123',
items: [],
totalAmount: 50,
status: 'PENDING'
});
await repo.save({
id: 'order2',
userId: 'user123',
items: [],
totalAmount: 100,
status: 'CONFIRMED'
});
await repo.save({
id: 'order3',
userId: 'user456',
items: [],
totalAmount: 75,
status: 'PENDING'
});
const orders = await repo.findByUserId('user123');
expect(orders).toHaveLength(2);
expect(orders.every(o => o.userId === 'user123')).toBe(true);
});
it('should return empty array when no orders found', async () => {
const orders = await repo.findByUserId('nonexistent');
expect(orders).toEqual([]);
});
});
describe('updateStatus', () => {
it('should update order status', async () => {
await repo.save({
id: 'order123',
userId: 'user123',
items: [],
totalAmount: 100,
status: 'PENDING'
});
await repo.updateStatus('order123', 'CONFIRMED');
const updated = await repo.findById('order123');
expect(updated.status).toBe('CONFIRMED');
});
});
});
契约测试(Contract Testing)
Pact消费者测试
// tests/contracts/orderService.consumer.test.js
const { Pact } = require('@pact-foundation/pact');
const path = require('path');
const axios = require('axios');
const { like, eachLike, term } = require('@pact-foundation/pact').Matchers;
const provider = new Pact({
consumer: 'order-service',
provider: 'inventory-service',
port: 1234,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'info'
});
describe('Order Service - Inventory Service Contract', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
describe('check stock availability', () => {
it('should return available when stock is sufficient', async () => {
// 定义期望的交互
await provider.addInteraction({
state: 'product p1 has 10 items in stock',
uponReceiving: 'a request to check stock for 5 items',
withRequest: {
method: 'POST',
path: '/api/stock/check',
headers: { 'Content-Type': 'application/json' },
body: {
items: [
{ productId: 'p1', quantity: 5 }
]
}
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
available: true,
details: eachLike({
productId: like('p1'),
requested: like(5),
available: like(10),
sufficient: true
})
}
}
});
// 执行实际的请求
const response = await axios.post('http://localhost:1234/api/stock/check', {
items: [{ productId: 'p1', quantity: 5 }]
});
expect(response.data.available).toBe(true);
expect(response.data.details[0].sufficient).toBe(true);
});
it('should return unavailable when stock is insufficient', async () => {
await provider.addInteraction({
state: 'product p1 has 3 items in stock',
uponReceiving: 'a request to check stock for 10 items',
withRequest: {
method: 'POST',
path: '/api/stock/check',
headers: { 'Content-Type': 'application/json' },
body: {
items: [
{ productId: 'p1', quantity: 10 }
]
}
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
available: false,
details: eachLike({
productId: like('p1'),
requested: like(10),
available: like(3),
sufficient: false
})
}
}
});
const response = await axios.post('http://localhost:1234/api/stock/check', {
items: [{ productId: 'p1', quantity: 10 }]
});
expect(response.data.available).toBe(false);
expect(response.data.details[0].sufficient).toBe(false);
});
});
});
// tests/contracts/inventoryService.provider.test.js
const { Verifier } = require('@pact-foundation/pact');
const app = require('../../app');
const { setupTestDB, teardownTestDB } = require('../helpers/db');
describe('Inventory Service - Pact Verification', () => {
let server;
beforeAll(async () => {
await setupTestDB();
// 准备测试数据
await Product.create({ id: 'p1', stock: 10 });
server = app.listen(8080, () => {
console.log('Provider service running on port 8080');
});
});
afterAll(async () => {
await teardownTestDB();
server.close();
});
it('should validate the expectations of Order Service', () => {
const opts = {
provider: 'inventory-service',
providerBaseUrl: 'http://localhost:8080',
pactUrls: [path.resolve(process.cwd(), 'pacts/order-service-inventory-service.json')],
publishVerificationResult: true,
providerVersion: '1.0.0',
stateHandlers: {
'product p1 has 10 items in stock': async () => {
await Product.update({ id: 'p1' }, { stock: 10 });
return { description: 'Product p1 stock set to 10' };
},
'product p1 has 3 items in stock': async () => {
await Product.update({ id: 'p1' }, { stock: 3 });
return { description: 'Product p1 stock set to 3' };
}
}
};
return new Verifier(opts).verifyProvider().then(output => {
console.log('Pact Verification Complete!');
console.log(output);
});
});
});
性能测试
k6负载测试
// tests/performance/orders-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// 自定义指标
const errorRate = new Rate('errors');
const orderCreationDuration = new Trend('order_creation_duration');
export const options = {
stages: [
{ duration: '1m', target: 50 }, // 逐渐增加到50个虚拟用户
{ duration: '3m', target: 50 }, // 保持50个虚拟用户3分钟
{ duration: '1m', target: 100 }, // 增加到100个虚拟用户
{ duration: '3m', target: 100 }, // 保持100个虚拟用户3分钟
{ duration: '1m', target: 0 }, // 逐渐减少到0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95%请求应在500ms内完成
errors: ['rate<0.1'], // 错误率应小于10%
order_creation_duration: ['p(95)<1000']
}
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
// 获取认证token
const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
username: 'testuser',
password: 'password'
}), {
headers: { 'Content-Type': 'application/json' }
});
check(loginRes, {
'login successful': (r) => r.status === 200
});
const token = JSON.parse(loginRes.body).token;
// 创建订单
const orderPayload = {
items: [
{ productId: 'p1', quantity: 2, price: 50 },
{ productId: 'p2', quantity: 1, price: 100 }
],
shippingAddress: {
street: '123 Test St',
city: 'Test City',
country: 'Test Country'
}
};
const startTime = Date.now();
const orderRes = http.post(`${BASE_URL}/api/orders`, JSON.stringify(orderPayload), {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
const duration = Date.now() - startTime;
orderCreationDuration.add(duration);
const success = check(orderRes, {
'order created': (r) => r.status === 201,
'order has id': (r) => JSON.parse(r.body).id !== undefined,
'response time < 1s': (r) => r.timings.duration < 1000
});
errorRate.add(!success);
if (success) {
const orderId = JSON.parse(orderRes.body).id;
// 查询订单
const getRes = http.get(`${BASE_URL}/api/orders/${orderId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
check(getRes, {
'order retrieved': (r) => r.status === 200
});
}
sleep(1); // 模拟用户思考时间
}
// 运行测试:k6 run orders-load-test.js
混沌测试与韧性验证
性能测试验证的是系统在正常负载下的表现,而**混沌测试(Chaos Engineering)**验证的是系统在异常条件下的自愈能力。通过主动注入故障,提前发现系统的单点故障和级联风险。
// 使用 Chaos Monkey 风格的 API 故障注入
// tests/chaos/fault-injection.test.js
const { execSync } = require('child_process');
describe('API Resilience', () => {
beforeAll(async () => {
await setupTestDB();
});
test('handles database connection failure gracefully', async () => {
// 模拟数据库断开
execSync('docker pause test-postgres');
const response = await request(app)
.get('/api/orders')
.set('Authorization', `Bearer ${token}`);
// 应返回 503 而非 500,且不应泄露堆栈
expect(response.status).toBe(503);
expect(response.body).toHaveProperty('error');
expect(response.body.error).not.toContain('at '); // 不泄露堆栈
execSync('docker unpause test-postgres');
});
test('circuit breaker opens after consecutive failures', async () => {
// 模拟下游服务持续故障
nock(process.env.PAYMENT_SERVICE_URL)
.post('/api/charge')
.times(5)
.reply(500);
// 前 5 次请求失败
for (let i = 0; i < 5; i++) {
await request(app).post('/api/orders').send(validOrder);
}
// 断路器应打开,第 6 次请求直接拒绝
const response = await request(app)
.post('/api/orders')
.send(validOrder);
expect(response.status).toBe(503);
expect(response.body.error).toContain('Service temporarily unavailable');
});
});
故障注入场景清单:
| 故障类型 | 注入方式 | 验证目标 |
|---|---|---|
| 网络延迟 | tc qdisc add dev eth0 root netem delay 500ms | 超时处理、降级策略 |
| 服务断开 | docker pause <container> | 优雅降级、用户友好错误 |
| CPU 满载 | stress-ng --cpu 8 | 响应时间恶化时是否限流 |
| 内存不足 | stress-ng --vm 2 --vm-bytes 1G | OOM 处理、请求排队机制 |
| 依赖故障 | Nock 模拟 5xx/timeout | 断路器、重试退避 |
实践建议:混沌测试应在预发布环境执行,每次注入单一故障,观察恢复时间(MTTR)。使用 OpenTelemetry 追踪请求链路,验证故障是否被正确隔离。
TDD 实践:红绿重构循环
测试驱动开发(TDD)不是"先写测试再写代码"的形式主义,而是一种通过测试明确需求、驱动设计的工程方法。TDD 的核心循环由三步组成:
- Red:编写一个会失败的测试,明确需求边界
- Green:用最简单的代码让测试通过
- Refactor:在不改变行为的前提下优化代码结构
// Step 1: Red - 写测试(此时 OrderService 还不存在)
describe('OrderService', () => {
it('calculates total with tax', () => {
const service = new OrderService();
const items = [
{ price: 100, quantity: 2, taxable: true },
{ price: 50, quantity: 1, taxable: false },
];
const total = service.calculateTotal(items, 0.1);
// 税前 250,税 20,总计 270
expect(total).toBe(270);
});
});
// Step 2: Green - 最小实现
class OrderService {
calculateTotal(items, taxRate) {
return items.reduce((sum, item) => {
const subtotal = item.price * item.quantity;
const tax = item.taxable ? subtotal * taxRate : 0;
return sum + subtotal + tax;
}, 0);
}
}
// Step 3: Refactor - 提取函数、命名优化(保持测试通过)
class OrderService {
calculateTotal(items, taxRate) {
return items.reduce((sum, item) => sum + this._lineTotal(item, taxRate), 0);
}
_lineTotal(item, taxRate) {
const subtotal = item.price * item.quantity;
const tax = item.taxable ? subtotal * taxRate : 0;
return subtotal + tax;
}
}
TDD 的适用边界:
| 场景 | TDD 效果 | 原因 |
|---|---|---|
| 算法核心逻辑 | ⭐⭐⭐⭐⭐ | 边界条件多,测试驱动清晰 |
| 数据转换/验证 | ⭐⭐⭐⭐ | 输入输出明确,易于断言 |
| UI 组件 | ⭐⭐⭐ | 交互复杂,快照测试辅助 |
| 数据库访问层 | ⭐⭐ | 行为依赖于外部状态 |
| 探索性/原型代码 | ⭐ | 需求不确定,测试反而成为负担 |
测试覆盖率配置
// jest.config.js
module.exports = {
testEnvironment: 'node',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./services/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/config/**'
],
coverageReporters: ['text', 'lcov', 'html'],
testMatch: ['**/tests/**/*.test.js']
};
总结
完善的API测试策略应包含:
- 单元测试:快速验证核心业务逻辑,覆盖率目标80%+
- 集成测试:验证组件协作和外部依赖,使用mock隔离
- 契约测试:保证服务间接口兼容性,防止破坏性变更
- 端到端测试:验证关键用户流程,数量要少
- 性能测试:验证系统在高负载下的表现
关键原则:
- 遵循测试金字塔,单元测试占大多数(70%),确保快速反馈
- 使用 mock 隔离外部依赖,提高测试速度和稳定性
- 契约测试防止服务间的不兼容变更,是微服务协作的基石
- 自动化所有测试,集成到 CI/CD 流程,每次提交都自动验证
- 定期运行性能测试,监控系统容量基线,提前发现性能退化
- 混沌测试验证系统韧性,假设故障一定会发生并提前演练
- TDD 在需求明确的算法和数据层价值最大,探索性开发可降低测试密度
延伸阅读
- Testing Strategies in a Microservice Architecture - Martin Fowler
- Pact文档
- k6文档
- Jest文档
- Supertest文档
- https://plumephp.com/github-actions-nodejs-ci/ — GitHub Actions CI 流水线自动化
- https://plumephp.com/event-driven-architecture-domain-events/ — 事件驱动架构与微服务测试
- https://plumephp.com/python-testing/ — Python 测试最佳实践
- https://plumephp.com/docker-compose-production/ — Docker 容器化测试环境搭建
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。