Before we get to unit testing, let’s recognize two concepts in agile software development.

BDD and TDD

The test is divided into two schools, which are:

1.1 BDD: Behavior-driven development

Do the implementation first, then test the implementation as thoroughly as possible. Behavior-driven development emphasizes what role to play, what function to want, and what to benefit from, focusing on whether the ultimate implementation of the entire system is consistent with user expectations.

1.2 TDD: Test-driven development

Emphasize test driven, write test cases first, according to test case driven development, have a good grasp of development details. A typical TDD process is illustrated in the following figure:

Process description:

  • Developers start by writing some test code (green circles)
  • Developers execute test cases, and then, of course, those test cases fail because the classes and methods mentioned in the tests are not implemented
  • The developer starts implementing the methods mentioned in the test case
  • The developer writes a function point and, with luck, the previous test case relative to it passes
  • Developers can refactor the code and add comments to finish the work

What are unit tests

Unit testing refers to checking and verifying the smallest testable unit in software, also known as module testing. In Node.js, a function, module, or API is verified correctly to ensure the availability of code.

2.1 Function of unit test

Unit testing is a necessary tool to improve code quality and minimize the most damage to legacy functionality. It is a common phenomenon for developers to fall into the trap of thinking when writing code. However, when writing tests, they tend to consider various situations. The main functions of unit testing are:

  • Verify the correctness of the code (for all possible inputs, once the test is covered, its output can be identified)
  • Determine whether changes to the code affect the determined results to avoid errors when modifying the code in individual collaborative development.
  • With API upgrades, it’s a good way to check your code for backwards compatibility
  • Simplify the debugging process

2.2 Necessity of unit testing

Common front-end libraries such as LoDash, KOA, and React have higher requirements for code robustness and quality, and unit testing is a prerequisite. In addition to library development, unit testing is also important in Node.js applications, especially during rapid iterations of projects, where each test case provides a layer of security for the application’s documentation.

2.2.1 Which projects need to introduce unit testing

  • The common dependency library, the SDK, the tool library, must introduce unit testing
  • For engineering applications, exposed apis and public methods, unit testing is best introduced

2.2.2 Where should unit tests be introduced in the program

  • During development, unit tests should test for things that can go wrong, or boundary cases
  • During maintenance, unit tests should follow our bug report, and each bug should have a UnitTest. Therefore, programmers will have two confidence in their code changes. One is that the bug will be fixed, and the other is that the same bug will not appear again.

How do you do unit tests

The basic syntax of node.js test cases consists of the following four basic statements:

  • describe: Defines a test suite
  • it: Defines a test case
  • expect: The judgment condition of the assertion
  • toEqual: Comparison results of assertions

Schematic diagram of test suite and test case relationship:

Unit testing tools

There are many front-end testing tools, common testing tools can be roughly divided into assertion library, testing framework, testing AIDS, testing coverage and other categories.

  • Assertion library: Provides semantically defined methods for making various judgments about the values being tested. These semantic methods return the results of the test, either success or failure.
  • Testing framework: Provides some convenient syntax for describing test cases and grouping use cases.
  • Test coverage tools: Test coverage tools are used to tally test cases against code and generate reports.
  • Test AIDS: Test Spy, test mock and other AIDS.

3.1 assertions library

Assertions are a core part of the unit testing framework. Generally, assertions return Boolean values, can also be comparison, null, re, etc. Assertion failure will cause the test to fail, or report error messages.

3.1.1 Basic types of assertion statements

There are generally four types of assertion statements (take Expect as an example)

  1. Equivalence assertion
expect(sth).toEqual(value)
expect(sth).not.toEqual(value)
Copy the code
  1. Comparative assertion
expect(sth).toBeGreaterThan(number)
expect(sth).toBeLessThanOrEqual(number)
Copy the code
  1. Type assertion
expect(sth).toBeInstanceOf(Class)
Copy the code
  1. Conditional testing
expect(sth).toBeTruthy()
expect(sth).toBeFalsy()
expect(sth).toBeDefined()
Copy the code

Four common assertion libraries are described below

  • Assert: Node’s natively supported assertion module supports TDD

The Assert module is the basis for most unit tests in Node, and is used by many third-party testing frameworks, even without one.

Assert. Ok (add (1, 1)); Assert. Equal (add (1, 1), 2);Copy the code
  • Should: support BDD
(add(1, 1)).should.be.a.Number();
(add(1, 1)).should.equal(2);
Copy the code
  • Expect: support BDD
expect(add(1, 1)).to.be.a("number")
expect(add(1, 1)).to.equal(2);
Copy the code
  • Chai: Supports browser and Node assertion libraries, TDD and BDD

Compared with assert, should.js and expect. Js have stronger semantics and support type detection, while should.js is more concise in syntax and supports chain syntax.

3.2 Test Framework

A test case contains one assertion or more assertions, so how do you organize multiple test cases? This is where a testing framework is needed, through which test cases can be tested in groups and test reports produced.

Here are four common testing frameworks

3.2.1 mocha The official documentation

Features:

  • Very rich function, support BDD, TDD
  • Supports running in Node.js and browsers
  • Very friendly for asynchronous testing support
  • Support 4 kinds of hooks, including before/after/beforeEach/afterEach
// introduce the module or class to be tested const add = require("./add"); // assert: nodejs built-in assert module const assert = require("assert"); Function () {before(function() {// runs before all tests in this block}); // it: define a test case it("1 plus 1 should equal 2", function() {assert.equal(add(1, 1), 2); }); after(function() { // runs after all test in this block }); });Copy the code

3.2.2 Jasmine

Jasmine is a full-featured testing framework with built-in assertions Expect; However, with global declaration and configuration, it is more complex to use and less flexible.

const add = require(".. /src/add"); Describe (" add function test ", function() {it("1 + 1 = 2", function() {expect(add(1, 1)).toequal (2); }); Function () {expect(add(1, 1)).toequal (jasmine. Any (Number)); }); });Copy the code

3.2.3 ava

While javaScript is single-threaded, the asynchronous nature of Node.js allows for parallel IO. AVA takes advantage of this to make your tests run concurrently, which is especially useful for IO heavy testing, with the following features:

  • Lightweight, efficient and simple.
  • Concurrent tests, forcing atomic tests to be written
  • There are no hidden global variables, and each test file is environment independent
  • Supports ES7, Promise, Generator, Async, Observable
  • Built-in assertion strengthens assertion information
import test from 'ava'; function trimAll(string) { return string.replace(/[\s\b]/g, ''); } test('trimAll testing', TrimAll (' \n \r \t \v \b \f b arr I or \n \r \t \v \b \f '), 'Barrior'); TrimAll ('Barrior'), 'Barrior'); // If there are no null characters, the output value should be the input value T.iis (trimAll('Barrior'), 'Barrior'); TrimAll (new String(' Tom')), 'Tom'); // When entering other non-string data types, Should throw an error [undefined, null, 0, true, [], {}, () = > {}, Symbol ()], forEach (type = > {t.t hrows (() = > {trimAll (type); }); }); });Copy the code

Test (): Executes a test with the first argument as the title and the second argument as the use-case function, receiving a single argument, t, that contains the built-in assertion API; By convention this parameter is called t, and there is no need to rename it.

3.2.4 The most popular front-end testing frameworkThe official documentation

A full-featured ‘zero configuration’ testing framework used by almost all of the country’s major Internet companies

Jest is a testing framework produced by Facebook. Compared with other testing frameworks, the biggest feature of Jest is that it has built-in common testing tools, such as built-in assertion Expect, test coverage tool, UI testing tool, mock ability, etc. At the same time, it can be collected into many plug-ins. Working with mainstream software libraries (vscode) like TypeScript, React, Vue, etc., really works out of the box.

Jest has clearer documentation than Mocha and supports parallel testing as efficiently as AVA

Jest supports asynchronous testing well, using examples

// fetchdata.test. js import {fetchData} from './fetchData' test('fetchData returns result {success: true }', async () => { // fetchData().then(res => console.log(res)) await expect(fetchData()).resolves.toMatchObject({ data: { success: True}})}) test('fetchData returns 404', Async () => {await expect(fetchData()).rejects. ToThrow (' Request failed with status code 404')}) test('fetchData returns the result as follows  { success: true }', async () => { const response = await fetchData() expect(response.data).toEqual({ success: True})}) test('fetchData returns 404', Async () => {expect. Assertions (1) // Enforce catch expect try {await fetchData()} catch (e) { console.log(e.toString()) // Error: Request failed with status code 404 expect(e.toString()).toEqual('Error: Request failed with status code 404') }})Copy the code

3.3 HTTP Request Emulation

When doing Web development with Node, it is essential to simulate HTTP requests, and it would be too low to use a browser for all requests.

  • superTest

SuperTest is a great node.js library for simulating Http requests that encapsulates the interface that sent the Http request and provides simple Expect assertions to determine what the interface returns.

The library is also used in the KOA source code

const request = require('supertest');
const assert = require('assert');
const Koa = require('koa');

describe('app.request', () => {
    const app1 = new Koa();
    app1.request.message = 'hello';
    const app2 = new Koa();

    it('should merge properties', () => {
        app1.use((ctx, next) => {
            assert.equal(ctx.request.message, 'hello');
            ctx.state = 204;
        });

        return request(app1.listen())
            .get('/')
            .expect(404)
    })

    it('should not affect the original prototype', () => {
        app2.use((ctx, next) => {
            assert.equal(ctx.request.message, undefined);
            ctx.state = 204;
        })
        return request(app2.listen())
            .get('/')
            .expect(404)
    })
})
Copy the code

3.4 Test AIDS

In Node.js applications, there are always references between code and modules. In unit tests, however, we might not need to relate the execution and result of an internally called method, just to know that it was called correctly, or even to specify the return value of that function. At this point, using mock functions is essential.

Mock functions provide three features that are useful when writing test code:

  • Capture function calls
  • Sets the return value of the function
  • Change the internal implementation of a function

For example

// math.js
export const getAResult = () => {
  // a logic here
};
export const getBResult = () => {
  // b logic here
};
// caculate.js
import { getAResult, getBResult } from "./math";
export const getABResult = () => getAResult() + getBResult();
Copy the code

Here, getAResult() and getBResult() are the dependencies of the getABResult function. If we focus on the getABResult function, we should strip out the getAResult and getBResult mock to remove the dependency.

  • Sinon test helper library

Sinon replaces some of the functions or classes that we rely on in our code with test surrogates by creating test surrogates. Sinon has three main methods to assist us in testing: Spy, stub, and mock. Here are three of them:

  • Spy, which provides information about a function call but does not change the behavior of the function

By wrapping up a monitored function, it is clear how many times the function was called, what arguments were passed in, what results were returned, and even what exceptions were thrown.

  • Stub, which provides information about the call to the function, and can be used as an instance to make the stubbed function return any desired behavior.

A stub can simulate the unit test with a minimum of dependencies. For example, if a method may depend on the execution of another method that is transparent to us, it is good practice to use stubs to isolate it

var myObj = { prop: function() { return 'foo'; }}; sinon.stub(myObj, 'prop').callsFake(function() { return 'bar'; }); myObj.prop(); // 'bar'Copy the code

Fourthly, implement unit testing guidelines

The test examples for this article are simple. In practice, however, unit testing can be a pain. Changing code sometimes means that unit tests have to be changed, and new functions or apis have to be written to write new unit tests. In fact, it is unrealistic to expect all bugs to be eliminated by testing.

conclusion

Finally, make a simple summary of the content of this paper:

  • TDD/BDD is only a paradigm guide and should not be a limitation in application development
  • The first choice for testing frameworks is JEST
  • Mocha is an old framework that covers a wide range of scenarios and has a high level of community maturity. The combination of Mocha + Chai + Istanbul is also a good choice.
  • Interface unit test. Supertest is recommended for HTTP requests.
  • For the stability of application development and maintenance, it is necessary to design unit test specification reasonably and write test cases
  • The quality of software engineering is not determined by testing, but by design and maintenance.