08|单元测试:Serverle应用如何进行单元测试?
文章目录
这一讲我将带你学习如何为 Serverless 应用编写单元测试。
单元测试是保证代码质量和应用稳定性的重要手段,但很多同学却不喜欢写单元测试,觉得又麻烦,又要花很多时间,其实这与没有掌握正确的方法有很大关系。除此之外,还有的同学不知道怎么写单元测试,尤其是怎么对 Serverless 应用编写单元测试,而这是所有 Serverless 开发者面临的问题。 我们团队在使用 Serverless 的初期也踩过很多坑,总结起来主要有以下难点:
Serverless 架构是分布式的,组成 Serverless 应用的函数是单独运行的,这些函数集合到一起组成分布式架构,你需要对独立函数和分布式应用都进行测试;
Serverless 架构依赖很多云服务,比如各种 FaaS、BaaS 等,这些云服务很难在本地模拟;
Serverless 架构是事件驱动的,事件驱动这种异步工作模式也很难在本地模拟。
那怎么解决这些问题呢? 这就是今天这一讲的重点了。总的来说,这一讲我会先带你了解 Serverelss 应用的单元测试准则,这些准则可以指导你编写出更易测试的代码,然后会带你编写实际的单元测试,并总结出单元测试的一些最佳实践,让你能够学以致用。
Serverless 单元测试准则
著名的 Scrum 联盟创始人 Mike Cohn 在 2012 年提出了测试金字塔理论:
测试金字塔
如果你写过单元测试,测试金字塔对你来说肯定不陌生,测试可分为单元测试、服务测试和 UI 测试,金字塔越上层测试速度越慢,成本越高,所以你应该写更多的单元测试。
可是 Serverless 应用依赖很多云服务,函数参数也与触发器强相关,要怎么写代码才能更方便写单元测试呢?我实践总结出了几条测试准则,供你参考:
将业务逻辑和依赖的云服务分开,保持业务代码独立,使其更易于扩展和测试;
对业务逻辑编写充分的单元测试,保证业务代码的正确性;
对业务代码和云服务编写集成测试,保证应用的正确性。
我来带你看一个实际的例子。假设你要实现一个功能:保存用户信息,保存成功后并发送欢迎邮件。 最简单、最好实现、但不好测试的代码就是下面这样:
|
|
按照测试准则一,这份代码存在这样几个问题。
业务逻辑没有和 FaaS 服务分开: 因为 handler 是 FaaS 的入口函数,handler 的参数是由具体 FaaS 平台实现的,比如函数计算、Lambda 等,不同 FaaS 平台实现不一样。
单元测试依赖数据库(db)和邮件服务(mailer): 这些服务都需要发送网络请求。
所以让我们将这段代码进行重构,使其更易于测试:
|
|
// handler.js const db = require(‘db’).connect(); const mailer = require(‘mailer’); const Users = require(’./src/users’); // 初始化 User 实例 let users = new Users(db, mailer); module.exports.saveUser = (event, context, callback) => { users.save(event.email, callback); };
|
|
$ git clone git@github.com:nodejh/serverless-class.git $ cd 08/unit-testing/
|
|
. ├── README.md ├── tests │ └── users.test.js ├── src │ └── users.js ├── handler.js ├── node_modules ├── package.json └── serverless.yaml
|
|
const db = { saveUser: jest.fn((user, callback) => callback(null, 1)), }; const mailer = { sendWelcomeEmail: jest.fn(() => true), };
|
|
const Users = require(’../src/users’); test(‘用户信息写入数据库成功,发送邮件成功’, () => { // 模拟 db.saveUser,并调用成功 const db = { saveUser: jest.fn((user, cb) => cb(null, 1)), }; // 模拟 mailer.sendWelcomeEmail,并调用成功 const mailer = { sendWelcomeEmail: jest.fn(() => true), };
const users = new Users(db, mailer); const email = ’test@gmail.com’; users.save(email, (err, userId) => { // 第一个断言,保存用户信息后的结果为 null expect(err).toBeNull(); // 第二个断言,保存并发送 expect(userId).toBe(1); }); });
|
|
$ npm run test
unit-testing@1.0.0 test jest PASS test/users.test.js ✓ 用户信息写入数据库成功,发送邮件成功 (11 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 3.391 s Ran all test suites.
|
|
test(‘用户信息写入数据库成功,发送邮件失败’, () => { const db = { saveUser: jest.fn((user, cb) => cb(null, 1)), }; const mailer = { sendWelcomeEmail: jest.fn(() => false), };
const users = new Users(db, mailer);
const email = ’test@gmail.com’;
users.save(email, (err, userId) => {
expect(err).toBe(发送邮件(${email})失败
);
expect(userId).toBeUndefined();
});
});
test(‘用户信息写入数据失败’, () => { const db = { saveUser: jest.fn((user, cb) => cb(new Error(‘Internal Error’))), }; const mailer = { sendWelcomeEmail: jest.fn(() => false), };
const users = new Users(db, mailer); const email = ’test@gmail.com’; users.save(email, (err, userId) => { expect(err).toBe(‘保存用户信息失败’); expect(userId).toBeUndefined(); }); });
|
|
$ npm run test:coverage
unit-testing@1.0.0 test:coverage jest –collect-coverage PASS test/users.test.js ✓ 用户信息写入数据库成功,发送邮件成功 (7 ms) ✓ 用户信息写入数据库成功,发送邮件失败
✓ 用户信息写入数据失败 (1 ms) File % Stmts % Branch % Funcs % Lines Uncovered Line #s ———- ——— ———- ——— ——— ——————- All files 100 100 100 100 users.js 100 100 100 100 ———- ——— ———- ——— ——— ——————- Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 2.983 s Ran all test suites.
|
|
save(email, callback) {
// …
this.db.saveUser(user, (err, userId) => {
if (err) {
callback(‘保存用户信息失败’);
} else {
const success = this.mailer.sendWelcomeEmail(email);
if (success) {
// 发送邮件成功后,不再返回 userId
callback(null);
} else {
callback(发送邮件(${email})失败
);
}
}
});
}
|
|
$ npm run test
unit-testing@1.0.0 test jest FAIL test/users.test.js ✕ 用户信息写入数据库成功,发送邮件成功 (14 ms) ✓ 用户信息写入数据库成功,发送邮件失败 (1 ms) ✓ 用户信息写入数据失败 ● 用户信息写入数据库成功,发送邮件成功 expect(received).toBe(expected) // Object.is equality Expected: 1 Received: undefined 18 | expect(err).toBeNull(); 19 | // 第二个断言,保存并发送 > 20 | expect(userId).toBe(1); | ^ 21 | }); 22 | }); 23 | at callback (test/users.test.js:20:20) at cb (src/users.js:19:11) at Object.
(test/users.test.js:7:37) at Users.save (src/users.js:13:13) at Object. (test/users.test.js:16:9) Test Suites: 1 failed, 1 total Tests: 1 failed, 2 passed, 3 total Snapshots: 0 total Time: 3.054 s Ran all test suites.
|
|
. ├── README.md ├── tests │ └── controllers │ | └── users.test.js │ └── models │ | └── users.test.js │ └── views ├── src │ └── controllers │ | └── users.test.js │ └── models │ | └── users.test.js │ └── views ├── handler.js ├── node_modules ├── package-lock.json ├── package.json └── serverless.yml
|
|
文章作者 anonymous
上次更新 2024-06-15