1. Chai.js断言库:测试代码的艺术与科学
作为一名经历过无数个深夜调试的前端工程师,我深知好的测试工具对开发效率的影响。Chai.js正是那种能让测试代码从"必要之恶"变成"开发乐趣"的工具之一。它不仅仅是一个断言库,更是一种让测试表达更接近自然语言的哲学。
1.1 为什么我们需要专门的断言库?
在JavaScript生态中,测试代码的编写方式经历了几个阶段的演进:
javascript复制// 原始方式
if (result !== expected) {
throw new Error(`Expected ${expected}, but got ${result}`);
}
// 使用基础断言
console.assert(result === expected, 'Test failed');
// 使用Chai
expect(result).to.equal(expected);
Chai的价值在于:
- 可读性:测试代码读起来像自然语言描述
- 丰富断言:提供数十种专业断言方法
- 错误信息:自动生成详细的错误报告
- 扩展性:支持插件系统增加新功能
提示:好的测试代码应该像文档一样清晰,即使是不懂技术的产品经理也能理解测试意图
2. Chai的三种风格深度解析
2.1 Should风格:面向对象的优雅表达
Should风格通过扩展Object.prototype实现,其核心原理是给所有对象添加should属性:
javascript复制Object.defineProperty(Object.prototype, 'should', {
value: function() {
return new Assertion(this);
},
enumerable: false,
configurable: true
});
典型用法:
javascript复制user.should.be.an('object');
response.status.should.equal(200);
[1,2,3].should.include(2);
注意事项:
- 无法用于null/undefined测试
- 可能与其他修改Object.prototype的库冲突
- 在ES6类属性上使用时需要特殊处理
2.2 Expect风格:函数式接口的最佳实践
Expect风格采用纯函数式设计,不修改任何原型:
javascript复制function expect(val) {
return new Assertion(val);
}
实际应用示例:
javascript复制expect(user).to.be.an('object');
expect(response.status).to.equal(200);
expect([1,2,3]).to.include(2);
优势对比:
| 特性 | Should | Expect | Assert |
|---|---|---|---|
| 原型修改 | ✓ | ✗ | ✗ |
| null测试 | ✗ | ✓ | ✓ |
| 链式语法 | ✓ | ✓ | ✗ |
| 函数式风格 | ✗ | ✓ | ✓ |
2.3 Assert风格:传统TDD的坚守者
Assert风格适合习惯传统单元测试的开发者:
javascript复制assert.typeOf(user, 'object');
assert.equal(response.status, 200);
assert.include([1,2,3], 2);
适用场景:
- 从其他语言转来的开发者
- 需要明确函数调用的场景
- 团队已有TDD规范的项目
3. 核心断言方法实战指南
3.1 类型检查的深层原理
Chai的类型检查基于JavaScript的typeof和instanceof:
javascript复制// 基础类型检查
expect('test').to.be.a('string'); // typeof === 'string'
expect(123).to.be.a('number'); // typeof === 'number'
// 特殊类型处理
expect(null).to.be.a('null'); // 特殊处理
expect(undefined).to.be.an('undefined');
// 对象类型识别
expect([]).to.be.an('array'); // Array.isArray
expect(new Date()).to.be.a('date'); // instanceof
常见陷阱:
- NaN的类型是'number'
- 数组的typeof返回'object'
- 自定义类需要特殊断言
3.2 相等性检查的三种模式
理解不同相等性断言的区别至关重要:
javascript复制// == 相等
expect(1).to.equal('1'); // 通过,类型转换
expect(1).to.not.eql('1'); // 不通过
// === 严格相等
expect(1).to.not.strict.equal('1');
// 深度相等(递归比较)
expect({a:1}).to.deep.equal({a:1});
性能考虑:
- 简单类型:equal最快
- 复杂对象:deep.equal最可靠
- 大型对象:考虑针对性断言
3.3 异步测试的完整解决方案
现代JavaScript测试离不开异步场景处理:
javascript复制// Promise测试
it('should resolve with 42', function() {
return expect(Promise.resolve(42)).to.eventually.equal(42);
});
// async/await风格
it('should work with async', async function() {
const result = await someAsyncFunction();
expect(result).to.equal(42);
});
// 回调函数处理
it('should call callback', function(done) {
asyncFunction((err, res) => {
expect(res).to.equal(42);
done();
});
});
错误处理最佳实践:
javascript复制// 测试应该抛出的错误
expect(() => { throw new Error() }).to.throw();
// 测试Promise拒绝
expect(Promise.reject(new Error())).to.be.rejected;
4. 高级技巧与性能优化
4.1 链式语法的魔法解密
Chai的链式语法中有些词只是语法糖:
javascript复制expect(foo).to.be.true; // .to和.be可省略
expect(foo).true; // 等效写法
// 常用语法糖:
// .to, .be, .been, .is, .that, .which, .and, .has, .have
自定义链式方法:
javascript复制chai.use(function(_chai) {
_chai.Assertion.addProperty('awesome', function() {
this.assert(
this._obj === 'awesome',
'expected #{this} to be awesome',
'expected #{this} not to be awesome'
);
});
});
expect('awesome').to.be.awesome;
4.2 插件生态深度整合
Chai的插件系统极大扩展了其能力边界:
| 插件名称 | 用途 | 示例用法 |
|---|---|---|
| chai-as-promised | Promise支持 | expect(promise).eventually... |
| chai-http | HTTP接口测试 | expect(res).to.have.status(200) |
| chai-dom | DOM断言 | expect(el).to.have.class('active') |
| chai-sinon | 间谍/存根/模拟 | expect(spy).calledOnce |
| chai-jest-snapshot | Jest快照支持 | expect(obj).to.matchSnapshot() |
插件开发要点:
- 保持链式语法一致性
- 提供清晰的错误信息
- 考虑浏览器兼容性
- 完善的文档和示例
4.3 性能优化策略
在大规模测试套件中,断言性能变得重要:
-
避免不必要的深度比较:
javascript复制// 不推荐 - 全对象比较 expect(largeObj).to.deep.equal(expected); // 推荐 - 针对性断言 expect(largeObj.id).to.equal(123); expect(largeObj.name).to.equal('test'); -
复用断言对象:
javascript复制// 每次创建新实例 function test1() { expect(1).to.equal(1); } // 复用实例 const expect = require('chai').expect; function test2() { expect(1).to.equal(1); } -
使用断言规划:
javascript复制it('should have all properties', function() { const obj = {a:1, b:2}; expect(obj).to.have.keys(['a', 'b']); // 比多个has.property断言更快 });
5. 企业级应用实践
5.1 测试金字塔中的Chai
在不同测试层级中Chai的应用:
单元测试:
javascript复制// 纯函数测试
describe('utils', function() {
it('should add numbers', function() {
expect(add(1, 2)).to.equal(3);
});
});
集成测试:
javascript复制// API测试
describe('API', function() {
it('should return user data', async function() {
const res = await request.get('/user/1');
expect(res).to.have.status(200);
expect(res.body).to.have.property('name');
});
});
UI组件测试:
javascript复制// React组件测试
describe('Button', function() {
it('should render correctly', function() {
const wrapper = mount(<Button />);
expect(wrapper).to.have.className('btn');
expect(wrapper).to.not.be.disabled;
});
});
5.2 持续集成中的配置技巧
在CI环境中优化Chai测试:
-
错误格式化:
javascript复制chai.config.truncateThreshold = 0; // 显示完整差异 -
性能分析:
javascript复制// 使用--slow选项识别慢测试 mocha --slow 100 -
并行测试:
javascript复制// 避免全局状态污染 beforeEach(function() { this.expect = require('chai').expect; });
5.3 大型项目中的测试架构
可维护的测试代码结构:
code复制tests/
├── unit/
│ ├── utils/
│ ├── services/
│ └── ...
├── integration/
│ ├── api/
│ └── ...
└── e2e/
├── user-flow/
└── ...
共享断言模式:
javascript复制// test/helpers/common-assertions.js
module.exports = function expectUser(user) {
expect(user).to.have.keys(['id', 'name', 'email']);
expect(user.id).to.be.a('number');
expect(user.name).to.be.a('string');
};
6. 疑难问题深度剖析
6.1 对象比较的陷阱与解决方案
引用比较问题:
javascript复制const a = {x:1};
const b = {x:1};
expect(a).to.equal(b); // 失败
expect(a).to.deep.equal(b); // 通过
循环引用处理:
javascript复制const obj = {};
obj.self = obj;
// 默认会栈溢出
expect(obj).to.deep.equal({self: obj});
// 解决方案
expect(obj).to.have.property('self');
expect(obj.self).to.equal(obj);
6.2 浮点数比较的科学方法
绝对误差比较:
javascript复制// 不推荐
expect(0.1 + 0.2).to.equal(0.3); // 失败
// 推荐方式
expect(0.1 + 0.2).to.be.closeTo(0.3, 0.000001);
相对误差比较(自定义断言):
javascript复制chai.Assertion.addMethod('roughlyEqual', function(expected, tolerance = 0.01) {
const actual = this._obj;
const delta = Math.abs(actual - expected);
const percent = delta / Math.abs(expected);
this.assert(
percent <= tolerance,
`expected #{act} to be within ${tolerance*100}% of #{exp}`,
`expected #{act} to not be within ${tolerance*100}% of #{exp}`,
expected,
actual
);
});
expect(1.01).to.be.roughlyEqual(1.0, 0.02);
6.3 自定义错误消息的高级技巧
内联消息:
javascript复制expect(result, '用户登录应该返回token').to.have.property('token');
动态消息:
javascript复制expect(result).to.satisfy(function(val) {
assert(val > 0, `值应该大于0但得到${val}`);
return true;
});
上下文信息:
javascript复制chai.config.includeStack = true; // 显示完整调用栈
7. 测试驱动开发(TDD)实战
7.1 红-绿-重构循环中的Chai
TDD典型流程:
- 红阶段(编写失败测试):
javascript复制describe('Calculator', function() {
it('should add two numbers', function() {
const calc = new Calculator();
expect(calc.add(1, 2)).to.equal(3); // 先写断言
});
});
- 绿阶段(最小实现):
javascript复制class Calculator {
add(a, b) {
return 3; // 硬编码通过测试
}
}
- 重构阶段:
javascript复制class Calculator {
add(a, b) {
return a + b; // 真正实现
}
}
7.2 行为驱动开发(BDD)实践
BDD风格测试示例:
javascript复制describe('ShoppingCart', function() {
context('when empty', function() {
it('should have zero items', function() {
expect(cart.itemCount).to.equal(0);
});
it('should not allow checkout', function() {
expect(() => cart.checkout()).to.throw('empty');
});
});
context('with items', function() {
beforeEach(function() {
cart.addItem({name: 'Book', price: 10});
});
it('should calculate total', function() {
expect(cart.total).to.equal(10);
});
});
});
8. 测试覆盖率与质量保障
8.1 结合Istanbul的覆盖率测试
配置示例:
javascript复制// package.json
{
"scripts": {
"test": "nyc mocha",
"coverage": "nyc report --reporter=text-lcov | coveralls"
}
}
覆盖率阈值:
javascript复制// .nycrc
{
"check-coverage": true,
"lines": 80,
"statements": 80,
"functions": 80,
"branches": 70
}
8.2 静态分析与测试结合
使用ESLint确保测试质量:
javascript复制// .eslintrc.js
module.exports = {
env: {
mocha: true
},
rules: {
'no-unused-expressions': 'off', // 允许expect表达式
'max-nested-callbacks': ['error', 3] // 避免回调地狱
}
};
9. 浏览器环境特殊处理
9.1 跨浏览器测试策略
Karma配置示例:
javascript复制module.exports = function(config) {
config.set({
frameworks: ['mocha', 'chai'],
files: [
'src/**/*.js',
'test/**/*.spec.js'
],
browsers: ['Chrome', 'Firefox', 'Safari']
});
};
浏览器特定断言:
javascript复制if (typeof window !== 'undefined') {
expect(window).to.have.property('localStorage');
}
10. 未来演进与替代方案
10.1 Chai与现代测试框架对比
| 特性 | Chai | Jest Assertions | Node assert |
|---|---|---|---|
| 链式语法 | ✓ | ✓ | ✗ |
| 插件系统 | ✓ | ✗ | ✗ |
| 内置快照测试 | ✗ | ✓ | ✗ |
| 浏览器支持 | ✓ | ✓ | ✗ |
| 异步支持 | 需要插件 | 内置 | 有限 |
10.2 渐进式迁移策略
从Chai迁移到Jest的示例:
javascript复制// 旧代码
expect(result).to.deep.equal({a:1});
// 新代码
expect(result).toEqual({a:1}); // Jest风格
// 兼容层
jestExpect(result).toEqual({a:1}); // 同时使用
在大型项目中,我通常会先通过适配器模式实现渐进式迁移:
javascript复制// test-utils/expect.js
const jestExpect = require('expect');
module.exports = function expect(actual) {
const jestAssertion = jestExpect(actual);
return {
to: {
equal(expected) {
return jestAssertion.toEqual(expected);
},
// 其他适配方法...
}
};
};
11. 性能关键型应用的测试策略
对于高性能要求的应用,测试本身也需要优化:
11.1 基准测试集成
javascript复制describe('Performance', function() {
it('should process 1000 items under 50ms', function() {
const start = process.hrtime();
processItems(largeArray);
const [sec, nanosec] = process.hrtime(start);
const totalMs = (sec * 1000) + (nanosec / 1e6);
expect(totalMs).to.be.below(50);
});
});
11.2 内存泄漏检测
javascript复制afterEach(function() {
if (global.gc) {
global.gc();
}
expect(process.memoryUsage().heapUsed).to.be.below(
this.startMemory * 1.1 // 允许10%增长
);
});
12. 测试报告与可视化
12.1 自定义报告格式
使用mochawesome生成美观报告:
javascript复制// .mocharc.js
module.exports = {
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'reports',
overwrite: false,
html: true,
json: true
}
};
12.2 CI集成报告
GitLab CI示例配置:
yaml复制test:
stage: test
script:
- npm test
artifacts:
paths:
- coverage/
- reports/
expire_in: 1 week
13. 微服务架构下的测试挑战
在分布式系统中,Chai可以这样扩展:
13.1 契约测试
javascript复制describe('API Contract', function() {
it('should match schema', function() {
expect(response.body).to.matchSchema({
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' }
},
required: ['id']
});
});
});
13.2 跨服务断言
javascript复制const chaiHttp = require('chai-http');
chai.use(chaiHttp);
describe('Order Flow', function() {
it('should sync inventory', async function() {
const orderRes = await chai.request(orderService)
.post('/orders').send({item: 'A', qty: 1});
const inventoryRes = await chai.request(inventoryService)
.get('/items/A');
expect(inventoryRes.body.stock).to.equal(
initialStock - orderRes.body.qty
);
});
});
14. 测试代码的重构模式
14.1 工厂函数模式
javascript复制function createUserTest(userFactory) {
return function() {
const user = userFactory();
expect(user).to.have.property('id');
expect(user).to.have.property('name');
};
}
describe('User', function() {
it('should have required fields', createUserTest(() => ({
id: 1,
name: 'Test'
})));
});
14.2 组合式断言
javascript复制function expectValidUser(user) {
expect(user).to.be.an('object');
expect(user).to.include.keys(['id', 'name']);
expect(user.id).to.be.a('number').above(0);
expect(user.name).to.be.a('string').not.empty;
}
describe('User API', function() {
it('should return valid user', function() {
const user = getUser(1);
expectValidUser(user);
});
});
15. 安全测试实践
15.1 注入攻击检测
javascript复制describe('Security', function() {
it('should sanitize SQL input', async function() {
const maliciousInput = "1'; DROP TABLE users;--";
const res = await queryUser(maliciousInput);
// 应该返回空结果或错误,而不是执行注入
expect(res).to.not.contain('DROP TABLE');
});
});
15.2 敏感数据断言
javascript复制expect(response.body).to.not.have.property('password');
expect(response.body.token).to.not.include('secret');
16. 测试数据管理策略
16.1 夹具(Fixture)模式
javascript复制const testUsers = {
admin: { id: 1, role: 'admin' },
guest: { id: 2, role: 'guest' }
};
describe('Auth', function() {
it('should allow admin access', function() {
const res = login(testUsers.admin);
expect(res).to.have.property('access', true);
});
});
16.2 随机测试数据
javascript复制const { faker } = require('@faker-js/faker');
describe('User', function() {
for (let i = 0; i < 10; i++) {
it(`should handle random name (${i})`, function() {
const name = faker.name.fullName();
const user = createUser({ name });
expect(user.name).to.equal(name);
});
}
});
17. 测试金字塔的完整实现
17.1 单元测试层
javascript复制// 纯函数测试
describe('utils', function() {
it('should add numbers', function() {
expect(add(1, 2)).to.equal(3);
});
});
17.2 集成测试层
javascript复制// 数据库集成
describe('UserRepository', function() {
beforeEach(async function() {
await db.reset();
});
it('should save and retrieve user', async function() {
const repo = new UserRepository(db);
await repo.save({id: 1, name: 'Test'});
const user = await repo.findById(1);
expect(user).to.deep.equal({id: 1, name: 'Test'});
});
});
17.3 E2E测试层
javascript复制// 完整用户流程
describe('Checkout Flow', function() {
it('should complete purchase', async function() {
await page.goto('/products');
await page.click('.add-to-cart');
await page.goto('/checkout');
await page.fill('#credit-card', '4242424242424242');
await page.click('#submit');
await expect(page).to.have.url('/confirmation');
await expect(page.locator('.success')).toBeVisible();
});
});
18. 测试环境隔离策略
18.1 数据库隔离
javascript复制describe('UserService', function() {
let testDb;
beforeEach(async function() {
testDb = await createTestDatabase();
});
afterEach(async function() {
await testDb.destroy();
});
it('should not affect other tests', async function() {
const service = new UserService(testDb);
await service.create({name: 'Test'});
expect(await service.count()).to.equal(1);
});
});
18.2 网络请求模拟
javascript复制const nock = require('nock');
describe('WeatherService', function() {
beforeEach(function() {
nock('https://api.weather.com')
.get('/forecast')
.reply(200, { temp: 25 });
});
it('should get forecast', async function() {
const weather = await getForecast();
expect(weather.temp).to.equal(25);
});
});
19. 测试代码的可维护性技巧
19.1 描述性测试命名
javascript复制// 不好的命名
it('should work', function() { ... });
// 好的命名
it('should return HTTP 404 when resource does not exist', function() { ... });
19.2 测试代码重构
javascript复制// 重构前
it('should validate user', function() {
expect(validateUser({})).to.be.false;
expect(validateUser({name: 'A'})).to.be.false;
expect(validateUser({name: 'A', email: 'b@c'})).to.be.true;
});
// 重构后
describe('validateUser', function() {
const testCases = [
{ input: {}, expected: false },
{ input: {name: 'A'}, expected: false },
{ input: {name: 'A', email: 'b@c'}, expected: true }
];
testCases.forEach(({input, expected}) => {
it(`should return ${expected} for ${JSON.stringify(input)}`, function() {
expect(validateUser(input)).to.equal(expected);
});
});
});
20. 测试驱动开发的高级模式
20.1 参数化测试
javascript复制function addTest(a, b, expected) {
it(`should add ${a} and ${b} to get ${expected}`, function() {
expect(add(a, b)).to.equal(expected);
});
}
describe('add()', function() {
addTest(1, 2, 3);
addTest(-1, 1, 0);
addTest(0.1, 0.2, 0.3);
});
20.2 基于属性的测试
javascript复制const fc = require('fast-check');
describe('Math.abs', function() {
it('should always return non-negative', function() {
fc.assert(
fc.property(fc.integer(), (n) => {
return Math.abs(n) >= 0;
})
);
});
});
21. 前端组件测试的完整方案
21.1 React组件测试
javascript复制import { render, screen } from '@testing-library/react';
describe('Button', function() {
it('should show loading state', function() {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).to.have.attribute('disabled');
expect(screen.getByText('Loading...')).to.exist;
});
});
21.2 Vue组件测试
javascript复制import { mount } from '@vue/test-utils';
describe('Counter', function() {
it('should increment count', async function() {
const wrapper = mount(Counter);
await wrapper.find('button').trigger('click');
expect(wrapper.text()).to.include('Count: 1');
});
});
22. 后端API测试的完整流程
22.1 REST API测试
javascript复制const chaiHttp = require('chai-http');
chai.use(chaiHttp);
describe('User API', function() {
it('should create user', async function() {
const res = await chai.request(app)
.post('/users')
.send({name: 'Test'});
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
});
});
22.2 GraphQL测试
javascript复制describe('GraphQL', function() {
it('should query user', async function() {
const res = await chai.request(app)
.post('/graphql')
.send({
query: `{ user(id:1) { name } }`
});
expect(res.body.data.user).to.have.property('name');
});
});
23. 数据库测试的最佳实践
23.1 事务回滚模式
javascript复制describe('UserRepository', function() {
let transaction;
beforeEach(async function() {
transaction = await db.beginTransaction();
});
afterEach(async function() {
await transaction.rollback();
});
it('should save user', async function() {
const repo = new UserRepository(transaction);
await repo.save({name: 'Test'});
const user = await repo.findByName('Test');
expect(user).to.exist;
});
});
23.2 内存数据库测试
javascript复制const { MongoMemoryServer } = require('mongodb-memory-server');
describe('MongoDB', function() {
let mongoServer;
before(async function() {
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URI = mongoServer.getUri();
});
after(async function() {
await mongoServer.stop();
});
it('should work with real MongoDB', async function() {
const db = await connectToMongo();
expect(db).to.be.an('object');
});
});
24. 性能测试的完整方案
24.1 基准测试套件
javascript复制describe('Performance', function() {
this.timeout(0); // 禁用超时
it('should process 1000 items under 50ms', function() {
const data = Array(1000).fill().map((_, i) => i);
const start = process.hrtime();
processItems(data);
const [sec, ns] = process.hrtime(start);
const ms = (sec * 1000) + (ns / 1e6);
expect(ms).to.be.below(50);
});
});
24.2 负载测试集成
javascript复制const loadtest = require('loadtest');
describe('API Load', function() {
it('should handle 100rps', function(done) {
loadtest.loadTest({
url: 'http://localhost:3000/api',
maxRequests: 1000,
concurrency: 10,
statusCallback: (err, result) => {
if (err) return done(err);
expect(result.meanLatencyMs).to.be.below(200);
expect(result.errorRate).to.equal(0);
done();
}
});
});
});
25. 测试报告与质量门禁
25.1 多维度质量指标
javascript复制// 质量门禁配置示例
const QUALITY_GATES = {
unitTestCoverage: 80,
integrationTestCoverage: 60,
maxDuplication: 5,
maxComplexity: 10
};
describe('Quality Gates', function() {
it('should meet coverage requirements', function() {
const coverage = getCoverageReport();
expect(coverage.lines).to.be.at.least(QUALITY_GATES.unitTestCoverage);
});
it('should maintain code quality', function() {
const analysis = getStaticAnalysis();
expect(analysis.duplication).to.be.at.most(QUALITY_GATES.maxDuplication);
expect(analysis.complexity).to.be.at.most(QUALITY_GATES.maxComplexity);
});
});
25.2 趋势分析集成
javascript复制describe('Quality Trends', function() {
it('should not degrade coverage', function() {
const current = getCurrentCoverage();
const baseline = getBaselineCoverage();
expect(current.lines).to.be.at.least(baseline.lines - 5); // 允许5%波动
});
});
26. 测试代码的重构与维护
26.1 测试工具函数提取
javascript复制// test-utils/api-helpers.js
module.exports = {
async createTestUser(userData = {}) {
const defaultData = { name: 'Test', email: 'test@example.com' };
const res = await chai.request(app)
.post('/users')
.send({ ...defaultData, ...userData });
return res.body;
}
};
// 测试文件中
const { createTestUser } = require('./test-utils/api-helpers');
describe('User', function() {
it('should have default role', async function() {
const user = await createTestUser();
expect(user.role).to.equal('member');
});
});
26.2 测试数据工厂模式
javascript复制// test/factories/user-factory.js
class UserFactory {
static build(overrides = {}) {
return {
name: 'Test User',
email: 'user@test.com',
...overrides
};
}
static async create(overrides = {}) {
const user = this.build(overrides);
const res = await chai.request(app)
.post('/users')
.send(user);
return res.body;
}
}
// 测试文件中
describe('User', function() {
it('should allow admin creation', async function() {
const admin = await UserFactory.create({ role: 'admin' });
expect(admin.role).to.equal('admin');
});
});
27. 测试驱动开发的进阶模式
27.1 验收测试驱动开发(ATDD)
javascript复制describe('User Registration', function() {
it('should send welcome email', async function() {
// 1. 用户访问注册页面
await page.goto('/register');
// 2. 填写注册表单
await page.fill('#name', 'New User');
await page.fill('#email', 'new@user.com');
// 3. 提交表单
await page.click('#submit');
// 4. 验证结果
await expect(page).to.have.url('/welcome');
expect(lastEmailSent()).to.match(/Welcome New User/);
});
});
27.2 行为驱动开发(BDD)协作
javascript复制// 与业务人员协作编写的测试用例
feature('购物车结算', function() {
scenario('会员享受折扣', function() {
given('我是VIP会员', function() {
this.user = createUser({ level: 'VIP' });
});
when('我添加价值$100的商品', function() {
this.cart = createCart(this.user);
addItem(this.cart, 100);
});
then('总价应该是$90', function() {
expect(this.cart.total).to.equal(90);
});
});
});
28. 测试环境管理的高级策略
28.1 容器化测试环境
javascript复制const { GenericContainer } = require("testcontainers");
describe('Database Tests', function() {
let container;
let dbClient;
before(async function() {
this.timeout(30000);
container = await new GenericContainer("postgres")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password")
.start();
dbClient = new DbClient({
host: container.getHost(),
port: container.getMappedPort(5432)
});
});
after(async function() {
await container.stop();
});
it('should connect to test DB', async function() {
const res = await dbClient.query('SELECT 1');
expect(res).to.deep.equal([{ '?column?': 1 }]);
});
});
28.2 服务虚拟化
javascript复制const { MockServer } = require('mockserver-client');
describe('Payment Service', function() {
before(async function() {
this.mockServer = await MockServer.start();
await this.mockServer.mock({
path: '/payments',
method: 'POST',
response: {