API测试策略:从单元测试到契约测试的完整实践

全面讲解API测试的最佳实践,涵盖单元测试、集成测试、端到端测试、契约测试、性能测试,提供Jest、Supertest、Pact、k6等工具的实战代码和测试金字塔应用指南。

测试金字塔

测试金字塔:
                    ┌─────────┐
                    │  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 单元测试
TestcontainersPostgreSQL, 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 1GOOM 处理、请求排队机制
依赖故障Nock 模拟 5xx/timeout断路器、重试退避

实践建议:混沌测试应在预发布环境执行,每次注入单一故障,观察恢复时间(MTTR)。使用 OpenTelemetry 追踪请求链路,验证故障是否被正确隔离。

TDD 实践:红绿重构循环

测试驱动开发(TDD)不是"先写测试再写代码"的形式主义,而是一种通过测试明确需求、驱动设计的工程方法。TDD 的核心循环由三步组成:

  1. Red:编写一个会失败的测试,明确需求边界
  2. Green:用最简单的代码让测试通过
  3. 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测试策略应包含:

  1. 单元测试:快速验证核心业务逻辑,覆盖率目标80%+
  2. 集成测试:验证组件协作和外部依赖,使用mock隔离
  3. 契约测试:保证服务间接口兼容性,防止破坏性变更
  4. 端到端测试:验证关键用户流程,数量要少
  5. 性能测试:验证系统在高负载下的表现

关键原则:

  • 遵循测试金字塔,单元测试占大多数(70%),确保快速反馈
  • 使用 mock 隔离外部依赖,提高测试速度和稳定性
  • 契约测试防止服务间的不兼容变更,是微服务协作的基石
  • 自动化所有测试,集成到 CI/CD 流程,每次提交都自动验证
  • 定期运行性能测试,监控系统容量基线,提前发现性能退化
  • 混沌测试验证系统韧性,假设故障一定会发生并提前演练
  • TDD 在需求明确的算法和数据层价值最大,探索性开发可降低测试密度

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「backend」更多文章

  1. 零信任安全架构:从边界防御到身份中心的安全范式
  2. 混沌工程实践:构建高可用系统的故障注入与弹性测试
  3. 流式数据处理:Kafka Streams与Flink实战指南