nodejs 后端单元测试写法示例
个人见解: 对于一些复杂的业务逻辑还是应该写单元测试,以确保有将所有的情况考虑清楚
1. 安装测试依赖
npm i nyc mocha sinon chai -s
2. 代码示例
// A.js
class A {
add(a, b) {
return a + b;
}
}
module.exports = A;
// A.test.js
const sinon = require('sinon');
const expect = require('chai').expect;
const A = require('../A');
// 为每个测试用例创建独立的 mock 环境,为了测试用例之间的 stub 不相互干扰
let sandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
})
afterEach(() => {
sandbox.restore();
})
describe('unit test', () => {
it ('a + b', () => {
const a = new A();
const result = a.add(1, 2);
expect(result).to.equal(3);
})
// mock 对象的方法,返回指定的值
it ('mock add()', () => {
const a= new A();
sandbox.stub(a, 'add').returns(4);
const result = a.add(1, 2);
expect(result).to.equal(4);
})
})
3. 测试的执行
node_modules\.bin\mocha A.test.js
- mocha 没有全局安装,所以这样执行
- 执行参考可以指定测试文件,不指定默认会执行 test 目录下的测试文件
4. 实际应用中生成测试报告
package.json
中增加"test": "nyc --reporter=lcov mocha --exit"
其中lcov
格式的报告可以被 sonarqube 识别,从而利用 sonarscaner 将测试结果推送到 sonarqube 服务器- 运行
npm run test
即可在 coverage 目录看到测试结果
5. windows 下安装 sonarqube 服务器
参考: https://blog.csdn.net/JackSparrowlj/article/details/90512652
- 下载 sonarqube: http://www.sonarqube.org/downloads/
- 启动 sonarqube: windows-x86-64/StartSonar.bat
- 访问并修改密码:127.0.0.1:9000 默认密码: admin/admin 修改为 admin/root
- 创建登录 token -> 右上角 My Account -> Security -> Generate Tokens
6. windows 下安装 sonarscanner
参考: https://docs.sonarqube.org/latest/analysis/scan/sonarscanner/
- 解压后在 path 上加入 sonar-scanner 执行路径: bin/sonar-scanner.bat
7. 利用 sonarscanner 命令将测试报告推送到 sonarqube 服务器
sonar-scanner -D"sonar.projectKey=ut" -D"sonar.sources=src/common" -D"sonar.host.url=http://10.50.160.15:9000" -D"sonar.login=663dcd204cdbeeb09814ecc71316379077af5246" -D"sonar.javascript.lcov.reportPaths=./coverage/lcov.info"
- sonar-scanner 全局执行需要将 下载后的 sonarscanner 中 sonar-scanner.bat 加入到环境变量 path 上
- sonar.sources 指定扫描的文件
- sonar.host.url sonarqube 服务器
- sonar.login sonarqube 创建的 token
- sonar.javascript.lcov.reportPaths 测试报告的文件路径
- sonar.zaproxy.reportPath 可以指定 zaproxy 安全扫描的报告路径
- sonar.exclusions 扫描时排除的文件
sonar 其他配置方法
- 创建 sonar-project.properties 文件
- 配置 sonar-scanner 执行参数
sonar.projectKey=test
sonar.sources=src/app
sonar.host.url=http://127.0.0.1:9000
sonar.login=7cc73ced504aaeac63a73a17cde0d1773ca3734a
sonar.javascript.lcov.reportPaths=./coverage/ngv/lcov.info
- 配置后只需执行 sonar-scanner 命令即可,默认会去 sonar-project.properties 获取参数
Angular 生成测试报告并推送到 sonarqube
- 修改 karma.conf.js 生成 lcvo 格式报告
coverageReporter: {
dir: require('path').join(__dirname, './coverage/ngv'),
subdir: '.',
reporters: [
{ type: 'lcov' }, // lcov 会生成 html,lcov 格式; lcovonly 只生成 lcov, html 默认格式
{ type: 'text-summary' }
]
},
- 执行测试生成报告
ng test --code-coverage=true
- 推送测试结果到 sonarqube, 执行 sonar-scanner 命令即可
8. 带有 callback 的方法 mock 写法
cconst sinon = require('sinon');
// 因为 nodejs 同一个模块只会加载一次然后缓存起来,所以这里 mock fs 模块方法,那么其他模块再次使用该模块的时候使用的是缓存里面的已经mock了的
const fs = require('fs');
// 1. 有 callback 参数的方法调用,返回指定的值
// callsFake 后面的 function 参数为 readFile 的参数
// 调用 cb 则是调用 fs.readFile 时写的 callback 方法
sinon.stub(fs, 'readFile').callsFake((fileName, cb) => {
// 此处相当于手动调用 fs.readFile('file', (err, buf) => {}) 中的 callback 方法 => (err, buf) => {}
// 且可以自行调用时的传参可指定, fs.readFile 其结果实际上是通过 callback 带出来的
cb(null, Buffer.from('readFile')); // cb 的参数写法由 fs.readFile callback决定
});
fs.readFile('./test/A.test.js', (err, buf) => {
console.log(buf.toString()); // 结果 => readFile
})
// 2. 无 callback 方法 mock 返回指定的值
sinon.stub(fs, 'readFileSync').returns('test');
const data = fs.readFileSync('./test/A.test.js');
console.log(data.toString()); // 结果 => test
9. new Class 方法如何 Mock
首先要了解 js 中 class 是基于原型链实现的,class 中创建的方法实际上都在 Class.prototype 上,所有只需要 mock Class.prototype 上对应的方法则可以 mock new Class 创建实例所对应的所有方法
const sinon = require('sinon');
class Student {
work() {
return 'hard work'
}
}
// mock Student 所有实例的 work 方法
sinon.stub(Student.prototype, 'work').returns('test');
// 创建实例
const s = new Student();
console.log(s.work()); // 结果 => test (mock 成功).
10. 测试一个模块,根据 case 不同进行分组测试写法
describe('测试一个模块', function() {
describe('case 1', function() {
it('1.1', function(done) {
done();
})
it('1.2', function(done) {
done();
})
})
describe('case 2', function() {
it('2.1', function(done) {
done();
})
it('2.2', function(done) {
done();
})
})
})
测试结果:
11. 模块方法的 mock, 之后引用了该模块方法的地方都会被 mock
原理: nodejs 相同模块只会加载一次并缓存起来
参考: https://juejin.cn/post/6899752077807845383
// A.js
function add(a, b) {
return a + b;
}
module.exports = {add}
// B.js
const A = require('./A');
function add() {
return A.add(1, 1);
}
module.exports = {add}
// test.js
const sinon = require('sinon');
const A = require('./A');
const B = require('./B');
// 是对 A 模块 add 方法的 mock, 此时 B.add 中调用的 A.add 会被 mock 从而返回 3
// 要求 module.exports 的不是 Class, Class 需要通过 sionon.stub(Class.prototype, method) 方法来 mock
sinon.stub(A, 'add').returns(3);
console.log(B.add()); // 结果 => 3 而不是 2