测试类型

测试依据是否波及软件性能,分为 功能性测试非功能性测试,前者包含单元测试、集成测试、零碎测试、接口测试、回归测试、验收测试,后者包含文档测试、装置测试、性能测试、可靠性测试、安全性测试。功能性测试验证了性能逻辑自身是否正确,非功能性测试验证的是除性能之外的逻辑。测试是软件用户信念的重要起源,而自动化测试就是建设这种信念的最高效伎俩,联合 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.jsconst { commandSync } = require('execa');module.exports = () => {  commandSync('yarn sequelize db:migrate');};
// jest.config.jsmodule.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.jsconst 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 totalTests:       1 failed, 6 passed, 7 totalSnapshots:   0 totalTime:        3.06 sRan 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 totalTests:       7 passed, 7 totalSnapshots:   0 totalTime:        3.311 sRan all test suites matching /src\/controllers/i.# ...

这样就有了对店铺治理性能最根本的自动化测试,思考到 escapeHtmlInObject 办法的高频应用,须要对此办法编写性能测试用例:

// src/utils/escape-html-in-object.test.jsconst 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 totalTests:       1 failed, 5 passed, 6 totalSnapshots:   0 totalTime:        1.027 sRan 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.jsconst 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 objectTest Suites: 1 passed, 1 totalTests:       6 passed, 6 totalSnapshots:   0 totalTime:        1.021 sRan all test suites matching /src\/utils/i.# ...

性能测试

目前 escapeHtmlInObject 性能曾经正确执行,再次思考到此办法的高频应用,对此办法进一步做性能测试:

// src/utils/escape-html-in-object.perf.jsconst { 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.jsconst 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 objectTest Suites: 1 passed, 1 totalTests:       6 passed, 6 totalSnapshots:   0 totalTime:        1.049 sRan 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 服务器(十四):自动化测试