Build from scratch Node.js Enterprise web server (14): automated testing

Time:2021-1-15

Test type

According to whether the software function is involved, the test can be divided into two partsFunctional testingAndNon functional testingThe former includes unit test, integration test, system test, interface test, regression test and acceptance test, while the latter includes document test, installation test, performance test, reliability test and security test. Functional testing verifies the correctness of functional logic itself, while non functional testing verifies logic other than function. Testing is an important source of software users’ confidence, and automated testing is the most efficient way to build this confidence. Combined with CI, testing can be automatically executed at every time when the code changes to ensure the high quality of the project.

Build from scratch Node.js  Enterprise web server (14): automated testing

Write automation test

This chapter will be based on the work done in the previous chapterhost1-tech/nodejs-server-examples – 13-debugging-and-profilingusejestbenchmarkAdd the key functional and non functional automatic testing for store management, and execute the relevant module installation command in the project root directory

$yarn add - D jest superstest execa benchmark beautiful benchmark # locally install jest, superstest, benchmark, beautiful benchmark, execa
# ...
info Direct dependencies
├─ [email protected]
├─ [email protected]
├─ [email protected]
├─ [email protected]
└─ [email protected]
# ...

Test function

Now write a functional test for the key use cases of store management

$MKDIR tests # new tests to store test configuration scripts

$tree SRC - L 1 # shows the current directory content structure
.
├── 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 configuration
  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 ='mei Zhen Xiang ';

      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 ='mei Zhen Xiang ';

      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);
    });
  });
});

Perform the test:

$yarn test Src / controllers # perform function test under Src / controllers directory
# ...
_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: {"created at": "2020-08-16t08:00:05.063z", "Id": 470, "name": "meizhenxiang", "updated at": "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.
# ...

findcontrollers/shop › DELETE /:shopid › should delete shop infoIf the execution fails, optimize the logic according to the prompt, and then execute the test again (you can also use jest’s--watchParameter auto redo:

// 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 # perform function test under Src / controllers directory
# ...
_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.
# ...

In this way, we have the most basic automated testing of store management functions, considering theescapeHtmlInObjectIn order to use this method frequently, we need to write functional test cases for this method

// 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 # perform function test under Src / utils directory
# ...
_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.
# ...

findutils/escape-html-in-object › should keep none-string fields in object or arrayIf the execution fails, optimize the logic according to the prompt, and then execute the test again

// src/utils/escape-html-in-object.js
const escapeHtml = require('escape-html');

module.exports = function escapeHtmlInObject(input) {
  //Try to convert ORM object to normal object
  try {
    input = input.toJSON();
  } catch {}

  //Escaping values of type 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 # perform function test under Src / utils directory
# ...
_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.
# ...

performance testing

at presentescapeHtmlInObjectThe function has been implemented correctly. Considering the high frequency use of this method, the performance test of this method is further carried out

// 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 });

Perform the test:

$ node src/utils/escape-html-in- object.perf.js  #Execute 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)

It is found that the performance index is better than the underlying moduleescape-htmlIs lower by several orders of magnitude, walk through code doubttry-catchStatement causes memory allocation and release, resulting in poor performance, so try to useifStatement:

// src/utils/escape-html-in-object.js
const escapeHtml = require('escape-html');

module.exports = function escapeHtmlInObject(input) {
  //Try to convert ORM object to normal object
-  try {
-    input = input.toJSON();
-  } catch {}
+  if (input && typeof input == 'object' && typeof input.toJSON == 'function') {
+    input = input.toJSON();
+  }

  //Escaping values of type 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;
  }
};

Then run the test again:

$ node src/utils/escape-html-in- object.perf.js  #Execute 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)

It is found that the performance index is close to escape HTML, which indicates that the inference is correct.

Perform relevant functional tests and regression tests

$yarn test Src / utils # perform function test under Src / utils directory
# ...
_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.
# ...

Because the function test is passed, it shows that the function is maintained well, and this performance optimization has no impact on the original function.

Source code of this chapter

host1-tech/nodejs-server-examples – 14-testing

Read more

Build from scratch Node.js Enterprise web server (zero): static services
Build from scratch Node.js Enterprise web server (1): interface and layering
Build from scratch Node.js Enterprise web server (2): Verification
Build from scratch Node.js Enterprise web server (3): Middleware
Build from scratch Node.js Enterprise web server (4): exception handling
Build from scratch Node.js Enterprise web server (5): database access
Build from scratch Node.js Enterprise web server (6): session
Build from scratch Node.js Enterprise web server (7): authentication login
Build from scratch Node.js Enterprise web server (8): network security
Build from scratch Node.js Enterprise web server (9): configuration items
Build from scratch Node.js Enterprise web server (x): log
Build from scratch Node.js Enterprise web server (11): timed tasks
Build from scratch Node.js Enterprise web server (12): remote call
Build from scratch Node.js Enterprise web server (XIII): breakpoint debugging and performance analysis
Build from scratch Node.js Enterprise web server (14): automated testing
Build from scratch Node.js Enterprise web server (XV): summary and Prospect