网淘吧来吧,欢迎您!

Testing Patterns技能使用说明

2026-03-29 新闻来源:网淘吧 围观:9
电脑广告
手机广告

测试模式

编写能发现错误的测试,而非仅仅通过的测试。通过覆盖率获得信心,通过隔离性获得速度。


测试金字塔

层级比例速度成本可信度范围
单元测试约70%毫秒级低(隔离性)单一函数/类
集成测试约20%秒级中等中等模块边界、API、数据库
端到端测试约10%分钟级高(真实)完整的用户工作流

规则:如果你的端到端测试数量超过了单元测试,就翻转金字塔。


单元测试模式

核心模式

模式何时使用结构
安排-执行-断言所有单元测试的默认结构设置、执行、验证
给定-当-那么BDD风格,关注行为前置条件、操作、结果
参数化相同逻辑,多种输入数据驱动的测试用例
快照UI组件,序列化输出与保存的基线进行比较
基于属性数学不变量生成随机输入,验证属性

准备-执行-断言 (AAA)

每个单元测试的默认结构。设置、执行和验证的清晰分离使测试具有可读性和可维护性。

// Clean AAA structure
test('calculates order total with tax', () => {
  // Arrange
  const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }];
  const taxRate = 0.08;

  // Act
  const total = calculateTotal(items, taxRate);

  // Assert
  expect(total).toBe(27.0);
});

测试替身

根据情况使用正确类型的测试替身。每种类型服务于不同的目的。

类型目的何时使用示例
返回预设数据控制间接输入jest.fn().mockReturnValue(42)
模拟对象验证交互断言某方法被调用expect(mock).toHaveBeenCalledWith('arg')
间谍包装真实实现观察而不替换jest.spyOn(service, 'save')
伪造对象简化实现需要真实行为内存数据库,模拟HTTP服务器
// Stub — control indirect input
const getUser = jest.fn().mockResolvedValue({ id: 1, name: 'Alice' });

// Spy — observe without replacing
const spy = jest.spyOn(logger, 'warn');
processInvalidInput(data);
expect(spy).toHaveBeenCalledWith('Invalid input received');

// Fake — lightweight substitute
class FakeUserRepo implements UserRepository {
  private users = new Map<string, User>();
  async save(user: User) { this.users.set(user.id, user); }
  async findById(id: string) { return this.users.get(id) ?? null; }
}

参数化测试

当相同逻辑需要多种输入进行验证时,使用参数化测试。这消除了复制粘贴测试,同时提供了全面的覆盖范围。

// Vitest/Jest
test.each([
  ['hello', 'HELLO'],
  ['world', 'WORLD'],
  ['', ''],
  ['123abc', '123ABC'],
])('toUpperCase(%s) returns %s', (input, expected) => {
  expect(input.toUpperCase()).toBe(expected);
});
# pytest
@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("", ""),
])
def test_to_upper(input, expected):
    assert input.upper() == expected
// Go — table-driven tests (idiomatic)
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"zero", 0, 0, 0},
        {"negative", -1, -2, -3},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            if got := Add(tc.a, tc.b); got != tc.expected {
                t.Errorf("Add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.expected)
            }
        })
    }
}

集成测试模式

数据库测试策略

策略方法权衡
事务回滚将每个测试包装在事务中,结束后回滚速度快,但会隐藏提交错误
固定数据/种子数据在测试套件前加载已知数据可预测,但若模式更改则脆弱
工厂函数以编程方式生成数据灵活,但需要更多设置代码
Testcontainers在Docker中启动真实数据库现实,但启动较慢
// Transaction rollback pattern (Prisma)
beforeEach(async () => {
  await prisma.$executeRaw`BEGIN`;
});
afterEach(async () => {
  await prisma.$executeRaw`ROLLBACK`;
});

test('creates user in database', async () => {
  const user = await createUser({ name: 'Alice', email: 'a@b.com' });
  const found = await prisma.user.findUnique({ where: { id: user.id } });
  expect(found?.name).toBe('Alice');
});

API 测试

// Supertest (Node.js)
import request from 'supertest';
import { app } from '../src/app';

describe('POST /api/users', () => {
  it('creates a user and returns 201', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'alice@test.com' })
      .expect(201);

    expect(res.body).toMatchObject({
      id: expect.any(String),
      name: 'Alice',
    });
  });

  it('returns 400 for invalid email', async () => {
    await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'not-an-email' })
      .expect(400);
  });
});

模拟最佳实践

模拟边界,而非实现

基本原则:在系统边界处(外部 API、数据库、文件系统)进行模拟,切勿模拟内部领域逻辑。

// BAD — mocking internal implementation
jest.mock('./utils/formatDate');  // Breaks on refactor

// GOOD — mocking external boundary
jest.mock('./services/paymentGateway');  // Third-party API is the boundary

何时模拟与何时不模拟

需要模拟无需模拟
HTTP API、外部服务纯函数
数据库(在单元测试中)您自己的领域逻辑
文件系统、网络数据转换
时间/日期(Date.now简单计算
环境变量内部类方法

为可测试性而进行依赖注入

组织代码结构,以便在测试中能够替换依赖项。这是实现可测试代码最具影响力的单一模式。

// Injectable dependencies — easy to test
class OrderService {
  constructor(
    private paymentGateway: PaymentGateway,
    private inventory: InventoryService,
    private notifier: NotificationService,
  ) {}

  async placeOrder(order: Order): Promise<OrderResult> {
    const stock = await this.inventory.check(order.items);
    if (!stock.available) return { status: 'out_of_stock' };

    const payment = await this.paymentGateway.charge(order.total);
    if (!payment.success) return { status: 'payment_failed' };

    await this.notifier.send(order.userId, 'Order confirmed');
    return { status: 'confirmed', id: payment.transactionId };
  }
}

// In tests — inject fakes
const service = new OrderService(
  new FakePaymentGateway(),
  new FakeInventory({ available: true }),
  new FakeNotifier(),
);

框架速查表

框架语言测试类型测试运行器断言库
JestJS/TS单元/集成测试内置expect()
VitestJS/TS单元/集成测试Vite原生expect()(兼容Jest)
PlaywrightJS/TS/Python端到端测试内置expect()/ 定位器
CypressJS/TS端到端内置cy.should()
pytestPython单元/集成内置assert
Go 测试框架Go单元/集成go testt.Error()/ testify
RustRust单元/集成cargo testassert!()/assert_eq!()
JUnit 5Java/Kotlin单元/集成内置assertEquals()
RSpecRuby单元/集成内置expect().to
PHPUnitPHP单元/集成内置$this->assert*()
xUnitC#单元/集成内置Assert.Equal()

测试质量检查清单

质量规则原因
确定性相同输入每次产生相同结果不稳定的测试会侵蚀信任
隔离性测试间无共享可变状态依赖顺序的测试会在持续集成中失效
快速单元测试:< 10毫秒,集成测试:< 1秒,端到端测试:< 30秒缓慢的测试不会被执行
可读性强测试名称描述场景和预期结果测试即文档
易于维护修改一个行为,只需修改一个测试脆弱的测试会拖慢开发进度
专注性每个测试一个逻辑断言失败能准确定位问题

命名规范: test_[单元]_[场景]_[预期结果]当 [条件 Y] 时,应 [执行 X]


覆盖策略

何时以何为目标

目标时机依据
80%+ 行覆盖率业务逻辑、工具函数、核心领域高投资回报率 — 能捕获大多数回归问题
90%+ 分支覆盖率支付处理、认证、安全关键部分边界情况在此很重要
100% 覆盖率几乎从不——收益递减Getter/setter测试增加噪音,而非信心
变异测试高覆盖率后的关键路径验证测试确实能捕获缺陷

不应测试的内容

跳过原因
生成的代码(Prisma客户端,protobuf)由工具维护
第三方库内部实现非你的职责
简单的getter/setter无逻辑可验证
配置文件转而测试它们配置的行为
Console.log / 打印语句无业务价值的副作用

测试组织

src/
├── services/
│   ├── order.service.ts
│   └── order.service.test.ts      # Co-located unit tests
├── api/
│   └── routes/
│       └── orders.ts
tests/
├── integration/
│   ├── api/
│   │   └── orders.test.ts         # API integration tests
│   └── db/
│       └── order.repo.test.ts     # DB integration tests
├── e2e/
│   ├── pages/                     # Page objects
│   │   └── checkout.page.ts
│   └── specs/
│       └── checkout.spec.ts       # E2E specs
└── helpers/
    ├── factories.ts               # Test data factories
    └── setup.ts                   # Global test setup

规则:单元测试应与源代码放在一起。集成测试和端到端测试应分离到专门的目录中。


反模式

反模式问题修复
测试实现细节重构时测试失败,而非发现实际错误测试行为和输出,而非内部实现
不稳定的测试不确定性的失败会削弱对持续集成的信任消除时间/顺序/网络依赖
测试污染测试间共享的可变状态发生泄漏beforeEach/setUp
中重置状态在测试中使用休眠sleep(2000)速度慢且不可靠
巨型编排50行配置掩盖真实意图抽离工厂/构建器/夹具
无断言测试测试通过但未验证任何逻辑每个测试必须包含断言或期望
过度模拟模拟所有对象导致测试失真仅模拟外部边界
复制粘贴式测试重复测试会逐渐失效使用参数化测试或辅助函数
测试框架本身验证库代码功能测试你的业务逻辑,信任依赖项
忽视测试失败跳过xit@Disabled标记会不断累积修复或删除——绝不积压跳过的测试
与数据库紧密耦合模式变更时测试失败单元测试使用仓储模式+模拟对象
一个庞大的测试单个测试覆盖10种场景拆分为专注的、有明确命名的测试
未为缺陷修复编写测试回归问题后续重现每个缺陷修复都应配备回归测试

绝对禁止

  1. 绝对不要测试实现细节而非行为——测试必须验证代码的功能,而非其实现方式
  2. 绝对不要使用sleep()于测试中——应使用显式等待、轮询、事件或支持自动重试的断言
  3. 绝对不要在测试间共享可变状态——每个测试都应自行设置和清理其状态
  4. 绝对不要编写无断言的测试——没有断言的测试无法证明任何问题
  5. 永远不要模拟内部领域逻辑——只在系统边界处(网络、数据库、文件系统、时钟)进行模拟
  6. 永远不要在没有关联问题和重新启用计划的情况下跳过测试——跳过的测试会逐渐腐化成永久性的空白
  7. 永远不要让测试套件处于失败状态——在继续之前修复它,或者给出理由后将其移除
  8. 永远不要以追求100%覆盖率为目标——覆盖率百分比是工具,而非目标;关键路径上的强断言胜过无处不在的弱断言

总结

应做不应做
测试行为,而非实现模拟所见的一切
修复错误前先编写测试为了更快发布而跳过测试
保持测试快速且确定使用sleep()或共享状态
使用工厂方法生成测试数据跨测试复制粘贴设置
在系统边界进行模拟模拟内部函数
为测试命名要有描述性命名测试测试1,测试2
每次推送时在CI中运行测试仅在本地运行测试
删除或修复跳过的测试@skip永久累积
对变体使用参数化测试重复测试代码
为可测试性注入依赖硬编码依赖

记住:测试是安全网——一套快速、可靠的测试套件能让你无畏重构,并充满信心地发布。

免责申明
部分文章来自各大搜索引擎,如有侵权,请与我联系删除。
打赏
文章底部电脑广告
手机广告位-内容正文底部

相关文章

您是本站第325764名访客 今日有204篇新文章/评论