测试类型
测试依据是否波及软件性能,分为 功能性测试 与 非功能性测试,前者包含单元测试、集成测试、零碎测试、接口测试、回归测试、验收测试,后者包含文档测试、装置测试、性能测试、可靠性测试、安全性测试。功能性测试验证了性能逻辑自身是否正确,非功能性测试验证的是除性能之外的逻辑。测试是软件用户信念的重要起源,而自动化测试就是建设这种信念的最高效伎俩,联合 CI 能够在代码产生变更的每个时刻主动执行测试保障工程的高质量。
写入自动化测试
本章将基于上一章已实现的工程 host1-tech/nodejs-server-examples – 13-debugging-and-profiling 应用 jest、benchmark 为店铺治理加上要害的功能性与非功能性自动化测试,在工程根目录执行相干模块装置命令:
$ yarn add -D jest supertest execa benchmark beautify-benchmark # 本地装置 jest、supertest、benchmark、beautify-benchmark、execa
# ...
info Direct dependencies
├─ beautify-benchmark@0.2.4
├─ benchmark@2.1.4
├─ execa@4.0.3
├─ jest@26.4.0
└─ supertest@4.0.2
# ...
测试性能
当初就店铺治理的要害用例编写功能测试:
$ mkdir tests # 新建 tests 寄存测试配置脚本
$ tree src -L 1 # 展现当前目录内容构造
.
├── Dockerfile
├── database
├── node_modules
├── package.json
├── public
├── scripts
├── src
├── tests
└── yarn.lock
// tests/globalSetup.js
const {commandSync} = require('execa');
module.exports = () => {commandSync('yarn sequelize db:migrate');
};
// jest.config.js
module.exports = {globalSetup: '<rootDir>/tests/globalSetup.js',};
// src/config/index.js
// ...
const config = {
// ...
// 测试配置
test: {
db: {
logging: false,
+ storage: 'database/test.db',
},
},
// ...
};
// ...
// package.json
{
"name": "13-debugging-and-profiling",
"version": "1.0.0",
"scripts": {
"start": "node -r ./scripts/env src/server.js",
"start:inspect": "cross-env CLUSTERING='' node --inspect-brk -r ./scripts/env src/server.js","start:profile":"cross-env CLUSTERING=''0x -- node -r ./scripts/env src/server.js",
"start:prod": "cross-env NODE_ENV=production node -r ./scripts/env src/server.js",
+ "test": "jest",
"sequelize": "sequelize",
"sequelize:prod": "cross-env NODE_ENV=production sequelize",
"build:yup": "rollup node_modules/yup -o src/moulds/yup.js -p @rollup/plugin-node-resolve,@rollup/plugin-commonjs,rollup-plugin-terser -f umd -n'yup'"
},
// ...
}
// src/controllers/shop.test.js
const supertest = require('supertest');
const express = require('express');
const {commandSync} = require('execa');
const shopController = require('./shop');
const {Shop} = require('../models');
describe('controllers/shop', () => {
const seed = '20200725050230-first-shop.js';
let server;
beforeAll(async () => {commandSync(`yarn sequelize db:seed --seed ${seed}`);
server = express().use(await shopController());
});
afterAll(() => commandSync(`yarn sequelize db:seed:undo --seed ${seed}`));
describe('GET /', () => {it('should get shop list', async () => {
const pageIndex = 0;
const pageSize = 10;
const shopCount = await Shop.count({offset: pageIndex * pageSize});
const res = await supertest(server).get('/');
expect(res.status).toBe(200);
const {success, data} = res.body;
expect(success).toBe(true);
expect(data).toHaveLength(Math.min(shopCount, pageSize));
});
});
describe('GET /:shopId', () => {it('should get shop info', async () => {const shop = await Shop.findOne();
const res = await supertest(server).get(`/${shop.id}`);
expect(res.status).toBe(200);
const {success, data} = res.body;
expect(success).toBe(true);
expect(data.name).toBe(shop.name);
});
});
describe('PUT /:shopId', () => {it('should update if proper shop info give', async () => {const shop = await Shop.findOne();
const shopName = '美珍香';
const res = await supertest(server).put(`/${shop.id}?name=${encodeURIComponent(shopName)}`
);
expect(res.status).toBe(200);
const {success, data} = res.body;
expect(success).toBe(true);
expect(data.name).toBe(shopName);
});
it('should not update if shop info not valid', async () => {const shop = await Shop.findOne();
const shopName = '';
const res = await supertest(server).put(`/${shop.id}?name=${encodeURIComponent(shopName)}`
);
expect(res.status).toBe(400);
const {success, data} = res.body;
expect(success).toBe(false);
expect(data).toBeFalsy();});
});
describe('POST /', () => {it('should create if proper shop info given', async () => {const oldShopCount = await Shop.count();
const shopName = '美珍香';
const res = await supertest(server).post('/').send(`name=${shopName}`);
expect(res.status).toBe(200);
const {success, data} = res.body;
expect(success).toBe(true);
expect(data.name).toBe(shopName);
const newShopCount = await Shop.count();
expect(newShopCount - oldShopCount).toBe(1);
});
it('should not create if shop info not valid', async () => {
const shopName = '';
const res = await supertest(server).post('/').send(`name=${shopName}`);
expect(res.status).toBe(400);
const {success, data} = res.body;
expect(success).toBe(false);
expect(data).toBeFalsy();});
});
describe('DELETE /:shopid', () => {it('should delete shop info', async () => {const oldShopCount = await Shop.count();
const shop = await Shop.findOne();
const res = await supertest(server).delete(`/${shop.id}`);
expect(res.status).toBe(200);
const {success} = res.body;
expect(success).toBe(true);
const newShopCount = await Shop.count();
expect(newShopCount - oldShopCount).toBe(-1);
});
});
});
执行测试:
$ yarn test src/controllers # 执行 src/controllers 目录下的功能测试
# ...
_FAIL_ src/controllers/shop.test.js
controllers/shop
GET /
✓ should get shop list (37 ms)
GET /:shopId
✓ should get shop info (8 ms)
PUT /:shopId
✓ should update if proper shop info give (21 ms)
✓ should not update if shop info not valid (11 ms)
POST /
✓ should create if proper shop info given (20 ms)
✓ should not create if shop info not valid (4 ms)
DELETE /:shopid
✕ should delete shop info (13 ms)
● controllers/shop › DELETE /:shopid › should delete shop info
expect(received).toBe(expected) // Object.is equality
Expected: true
Received: {"createdAt": "2020-08-16T08:00:05.063Z", "id": 470, "name": "美珍香", "updatedAt": "2020-08-16T08:00:05.154Z"}
111 |
112 | const {success} = res.body;
> 113 | expect(success).toBe(true);
| ^
114 |
115 | const newShopCount = await Shop.count();
116 | expect(newShopCount - oldShopCount).toBe(-1);
at Object.<anonymous> (src/controllers/shop.test.js:113:23)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 6 passed, 7 total
Snapshots: 0 total
Time: 3.06 s
Ran all test suites matching /src\/controllers/i.
# ...
发现 controllers/shop › DELETE /:shopid › should delete shop info
执行失败,依据提醒优化逻辑,而后再次执行测试(也能够应用 jest 的 --watch
参数主动从新执行):
// src/services/shop.js
// ...
class ShopService {
// ...
async remove({id, logging}) {const target = await Shop.findByPk(id);
if (!target) {return false;}
- return target.destroy({logging});
+ return Boolean(target.destroy({ logging}));
}
// ...
}
// ...
$ yarn test src/controllers # 执行 src/controllers 目录下的功能测试
# ...
_PASS_ src/controllers/shop.test.js
controllers/shop
GET /
✓ should get shop list (39 ms)
GET /:shopId
✓ should get shop info (9 ms)
PUT /:shopId
✓ should update if proper shop info give (18 ms)
✓ should not update if shop info not valid (6 ms)
POST /
✓ should create if proper shop info given (20 ms)
✓ should not create if shop info not valid (3 ms)
DELETE /:shopid
✓ should delete shop info (9 ms)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 3.311 s
Ran all test suites matching /src\/controllers/i.
# ...
这样就有了对店铺治理性能最根本的自动化测试,思考到 escapeHtmlInObject
办法的高频应用,须要对此办法编写性能测试用例:
// src/utils/escape-html-in-object.test.js
const escapeHtml = require('escape-html');
const escapeHtmlInObject = require('./escape-html-in-object');
describe('utils/escape-html-in-object', () => {it('should escape a string', () => {
const input = `"'$<>`;
expect(escapeHtmlInObject(input)).toEqual(escapeHtml(`"'$<>`));
});
it('should escape strings in object', () => {
const input = {
a: `"'$<>`,
b: `<>$"'`,
c: {d: `'"$><`,},
};
expect(escapeHtmlInObject(input)).toEqual({a: escapeHtml(`"'$<>`),
b: escapeHtml(`<>$"'`),
c: {d: escapeHtml(`'"$><`),
},
});
});
it('should escape strings in array', () => {const input = [`"'$<>`, `<>&"'`, [`'"$><`]];
expect(escapeHtmlInObject(input)).toEqual([escapeHtml(`"'$<>`),
escapeHtml(`<>&"'`),
[escapeHtml(`'"$><`)],
]);
});
it('should escape strings in object and array', () => {
const input1 = {
a: `"'$<>`,
b: `<>$"'`,
c: [`'"$><`, { d: `><&'"`}],
};
expect(escapeHtmlInObject(input1)).toEqual({a: escapeHtml(`"'$<>`),
b: escapeHtml(`<>$"'`),
c: [escapeHtml(`'"$><`), {d: escapeHtml(`><&'"`) }],
});
const input2 = [`"'$<>`, `<>&"'`, { a: `'"$><`, b: [`><&'"`] }];
expect(escapeHtmlInObject(input2)).toEqual([escapeHtml(`"'$<>`),
escapeHtml(`<>&"'`),
{a: escapeHtml(`'"$><`), b: [escapeHtml(`><&'"`)] },
]);
});
it('should keep none-string fields in object or array', () => {
const input1 = {
a: `"'$<>`,
b: 1,
c: null,
d: true,
e: undefined,
};
expect(escapeHtmlInObject(input1)).toEqual({a: escapeHtml(`"'$<>`),
b: 1,
c: null,
d: true,
e: undefined,
});
const input2 = [`"'$<>`, 1, null, true, undefined];
expect(escapeHtmlInObject(input2)).toEqual([escapeHtml(`"'$<>`),
1,
null,
true,
undefined,
]);
});
it('should convert sequelize model instance as plain object', () => {
const input = {toJSON: () => ({a: `"'$<>`, b: `<>$"'`}),
};
expect(escapeHtmlInObject(input)).toEqual({a: escapeHtml(`"'$<>`),
b: escapeHtml(`<>$"'`),
});
});
});
$ yarn test src/utils # 执行 src/utils 目录下的功能测试
# ...
_FAIL_ src/utils/escape-html-in-object.test.js
utils/escape-html-in-object
✓ should escape a string (2 ms)
✓ should escape strings in object (1 ms)
✓ should escape strings in array
✓ should escape strings in object and array (1 ms)
✕ should keep none-string fields in object or array (3 ms)
✓ should convert sequelize model instance as plain object (1 ms)
● utils/escape-html-in-object › should keep none-string fields in object or array
TypeError: Cannot convert undefined or null to object
at Function.keys (<anonymous>)
16 | // } else if (input && typeof input == 'object') {17 | const output = {};
> 18 | Object.keys(input).forEach((k) => {
| ^
19 | output[k] = escapeHtmlInObject(input[k]);
20 | });
21 | return output;
at escapeHtmlInObject (src/utils/escape-html-in-object.js:18:12)
at forEach (src/utils/escape-html-in-object.js:19:19)
at Array.forEach (<anonymous>)
at escapeHtmlInObject (src/utils/escape-html-in-object.js:18:24)
at Object.<anonymous> (src/utils/escape-html-in-object.test.js:64:12)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 5 passed, 6 total
Snapshots: 0 total
Time: 1.027 s
Ran all test suites matching /src\/utils/i.
# ...
发现 utils/escape-html-in-object › should keep none-string fields in object or array
执行失败,依据提醒优化逻辑,而后再次执行测试:
// src/utils/escape-html-in-object.js
const escapeHtml = require('escape-html');
module.exports = function escapeHtmlInObject(input) {
// 尝试将 ORM 对象转化为一般对象
try {input = input.toJSON();
} catch {}
// 对类型为 string 的值本义解决
if (Array.isArray(input)) {return input.map(escapeHtmlInObject);
- } else if (typeof input == 'object') {+} else if (input && typeof input == 'object') {const output = {};
Object.keys(input).forEach((k) => {output[k] = escapeHtmlInObject(input[k]);
});
return output;
} else if (typeof input == 'string') {return escapeHtml(input);
} else {return input;}
};
$ yarn test src/utils # 执行 src/utils 目录下的功能测试
# ...
_PASS_ src/utils/escape-html-in-object.test.js
utils/escape-html-in-object
✓ should escape a string (2 ms)
✓ should escape strings in object (1 ms)
✓ should escape strings in array
✓ should escape strings in object and array (1 ms)
✓ should keep none-string fields in object or array (1 ms)
✓ should convert sequelize model instance as plain object
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 1.021 s
Ran all test suites matching /src\/utils/i.
# ...
性能测试
目前 escapeHtmlInObject
性能曾经正确执行,再次思考到此办法的高频应用,对此办法进一步做性能测试:
// src/utils/escape-html-in-object.perf.js
const {Suite} = require('benchmark');
const benchmarks = require('beautify-benchmark');
const escapeHtmlInObject = require('./escape-html-in-object');
const suite = new Suite();
suite.add('sparse special chars', () => {escapeHtmlInObject('&');
});
suite.add('sparse special chars in object', () => {escapeHtmlInObject({ _: '&'});
});
suite.add('sparse special chars in array', () => {escapeHtmlInObject(['&']);
});
suite.add('dense special chars', () => {escapeHtmlInObject(`"'&<>"'&<>""''&&<<>>`);
});
suite.add('dense special chars in object', () => {escapeHtmlInObject({ _: `"'&<>"'&<>""''&&<<>>`});
});
suite.add('dense special chars in object', () => {escapeHtmlInObject([`"'&<>"'&<>""''&&<<>>`]);
});
suite.on('cycle', (e) => benchmarks.add(e.target));
suite.on('complete', () => benchmarks.log());
suite.run({async: false});
执行测试:
$ node src/utils/escape-html-in-object.perf.js # 执行 escape-html-in-object.perf.js
6 tests completed.
sparse special chars x 39,268 ops/sec ±1.39% (73 runs sampled)
sparse special chars in object x 15,887 ops/sec ±1.11% (70 runs sampled)
sparse special chars in array x 19,084 ops/sec ±1.24% (75 runs sampled)
dense special chars x 39,504 ops/sec ±1.07% (89 runs sampled)
dense special chars in object x 16,127 ops/sec ±1.04% (87 runs sampled)
dense special chars in object x 20,288 ops/sec ±0.90% (94 runs sampled)
发现执行指标比底层模块 escape-html 的低了若干数量级,走查代码狐疑 try-catch
语句引起内存调配与开释导致性能变差,因而尝试应用 if
语句进行替换:
// src/utils/escape-html-in-object.js
const escapeHtml = require('escape-html');
module.exports = function escapeHtmlInObject(input) {
// 尝试将 ORM 对象转化为一般对象
- try {- input = input.toJSON();
- } catch {}
+ if (input && typeof input == 'object' && typeof input.toJSON == 'function') {+ input = input.toJSON();
+ }
// 对类型为 string 的值本义解决
if (Array.isArray(input)) {return input.map(escapeHtmlInObject);
} else if (input && typeof input == 'object') {const output = {};
Object.keys(input).forEach((k) => {output[k] = escapeHtmlInObject(input[k]);
});
return output;
} else if (typeof input == 'string') {return escapeHtml(input);
} else {return input;}
};
而后再次执行测试:
$ node src/utils/escape-html-in-object.perf.js # 执行 escape-html-in-object.perf.js
sparse special chars x 6,480,336 ops/sec ±1.19% (89 runs sampled)
sparse special chars in object x 4,597,185 ops/sec ±1.12% (85 runs sampled)
sparse special chars in array x 4,131,352 ops/sec ±0.73% (87 runs sampled)
dense special chars x 3,512,408 ops/sec ±0.42% (89 runs sampled)
dense special chars in object x 3,073,066 ops/sec ±0.45% (90 runs sampled)
dense special chars in object x 3,153,604 ops/sec ±0.42% (95 runs sampled)
发现性能指标与 escape-html 相近,表明推断正确。
执行相干功能测试进行回归测试:
$ yarn test src/utils # 执行 src/utils 目录下的功能测试
# ...
_PASS_ src/utils/escape-html-in-object.test.js
utils/escape-html-in-object
✓ should escape a string (2 ms)
✓ should escape strings in object
✓ should escape strings in array (1 ms)
✓ should escape strings in object and array
✓ should keep none-string fields in object or array (1 ms)
✓ should convert sequelize model instance as plain object
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 1.049 s
Ran all test suites matching /src\/utils/i.
# ...
因为功能测试执行通过,表明性能保持良好,本次性能优化对原有性能不产生影响。
本章源码
host1-tech/nodejs-server-examples – 14-testing
更多浏览
从零搭建 Node.js 企业级 Web 服务器(零):动态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异样解决
从零搭建 Node.js 企业级 Web 服务器(五):数据库拜访
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时工作
从零搭建 Node.js 企业级 Web 服务器(十二):近程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能剖析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试