This article was originally published at: github.com/bigo-fronte… Welcome to follow and reprint.

How to write eggJS unit tests well

preface

When I interview front-end students, I often come across candidates with nodeJS development experience, but few who have written unit tests. I wanted to write this article to put more emphasis on unit testing and delivering high-quality code.

If your project's unit test branch specification rate is at least 80%, I think the student's code quality awareness is particularly good.Copy the code

Why unit tests

Like the test pyramid, unit tests are the base.

Quote from the EggJS website

  • How is your code quality measured?
  • How do you ensure code quality?
  • Do you dare to refactor code at any time?
  • How do you make sure your refactored code is still correct?
  • Are you confident enough to release your code at any time without testing?

If the answers are hesitant, then unit testing is very much needed.

Especially for large NodeJS projects, after years of code iteration, the business logic is complex, and code changes can be easily triggered. Unit testing provides a layer of stability for the application. Don’t face the soul of QA: Why do you always have the most bugs?

Test preparation

Eggjs provides a great test module: egg-mock. Egg-mock /bootstrap allows you to quickly instantiate your app

// test/controller/home.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/controller/home.test.js'.() = > {
  // test cases
});
Copy the code

Customize mockServiceByData with getMockData

However, we know that to write a single test, we need to prepare a large amount of JSON data, so we extend mockServiceByData and getMockData methods on the basis of app. MockService

1. New test/global. Ts

Import bigoMock from ‘@bigo/bgegg-mock’;

import * as assert from 'assert';
import { app } from 'egg-mock/bootstrap';
import * as path from 'path';
import * as fs from 'fs';

class BigoMock {
  app;
  ctx;
  assert = assert; / / mount assert
  async before() {
    console.log('hello bigoMock');
    this.app = app;
    await app.ready();
    this.ctx = app.mockContext();
    return;
  }
  /** * The Service method returns the value *@param Service method class *@param MethodName methodName *@param FileName indicates the fileName (status) */
  mockServiceByData(service, methodName, fileName) {
    let serviceClassName = ' ';
    if (typeof service === 'string') {
      const arr = service.split('. ');
      serviceClassName = arr[arr.length - 1];
    }
    const servicePaths = path.join(serviceClassName, methodName);
    this.app.mockService(service, methodName, () = > {
      return this.getMockData(servicePaths, fileName);
    });
  }
  /** * Get the mock data from local test/mockData *@param Folder *@param FileName indicates the fileName */
  getMockData(folder, fileName) {
    return this.getJson(folder, fileName);
  }
  / * * * agreement from the test/mockData/service/methodName/fileName. The json data *@param Folder *@param FileName indicates the fileName */
  getJson(folder, fileName) {
    // The json suffix is appended by default
    console.log(path.extname(fileName));
    if(! path.extname(fileName)) { fileName = fileName +'.json';
    }
    const fullPaths = path.join(process.cwd(), 'test/mockData', folder, fileName);
    return fs.readFileSync(fullPaths, 'utf-8'); }}const bigoMock = new BigoMock();
(async function() {
  awaitbigoMock.before(); }) ();export default bigoMock;
Copy the code

2.mockServiceByData

import bigoMock from '. /.. /global';

describe('User Interface single test Case'.() = > {

  it('should mock fengmk1 exists'.() = > {
    / / return the test/mockData/user/get/success. The json
    bigoMock.mockServiceByData('user'.'get'.'success.json');

    return app.httpRequest()
      .get('/user? name=fengmk1')
      .expect(200)
      // Return user information that does not exist
      .expect({
        name: 'fengmk1'}); }); });Copy the code

3.getMockData

import bigoMock from '. /.. /global';

// TESTS=test/app/service/spider/githubIssues/index.test.ts npm test
describe('githubIssues Crawler Single Test Case '.() = > {

  it('HTML structure parsed successfully'.async() = > {/ / return the test/mockData/githubIssues/html_mock. Js
    const html = bigoMock.getMockData('githubIssues'.'html_mock.js');

    const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
    bigoMock.assert(result[0].title === 'Nginx Reverse Proxy implementation online Debugging');
    bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
  });

});
Copy the code

Write a Service test

If you write a Controller single test that returns ctx.body data from the user’s request to ==, the process involves controller, Service, and downstream interface calls. If there is too much branching logic, the data will have many intermediate states, which makes the single-test case to be prepared particularly complex, resulting in low single-test branching coverage. Each Service function is a single function with clear input and output results. As long as we have enough unit test code for Service, the single test coverage will naturally increase.

Of course, Controller, Helper, Extend, etc., must also have corresponding unit tests to ensure code quality.

To sum up, this paper will focus on service single test.

How do I execute a single test file

We know that executing the NPM run test (actually executing egg-bin test) will run all the test cases, but we usually only care about the execution of the current single test when we write it. We can run the following command from the command line to execute the specified test file

TESTS=test/app/service/spider/githubIssues/index.test.ts npm test
Copy the code

If we have many test cases in a single test file and only want to run one, we can use IT.only

import bigoMock from '. /.. /.. /.. /.. /global';

// TESTS=test/app/service/spider/githubIssues/index.test.ts npm test
describe('githubIssues Crawler Single Test Case '.() = > {

  it('HTML structure parsed successfully'.async() = > {const html = bigoMock.getMockData('githubIssues'.'html_mock.js');
    const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
    bigoMock.assert(result[0].title === 'Nginx Reverse Proxy implementation online Debugging');
    bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
  });

  // Only the use case will be executed
  it.only('Failed to parse HTML structure'.async() = > {const html = ' ';
    const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
    bigoMock.assert(result.length === 0);
  });

});
Copy the code

Note: Remember to remove only before committing the code, otherwise only the use case 😂 will be executed when NPM run test is executed

The mock input

1. The constant mock

A service method, which usually has multiple arguments, can simply be constructed as input arguments when calling a service

// Only the use case will be executed
it.only('Failed to parse HTML structure'.async() = > {const html = ' '; / / constant mock
  const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
  bigoMock.assert(result.length === 0);
});
Copy the code

2. The file mock

File mocks are convenient if the input object is complex, or if other single-test files can be reused

it('HTML structure parsed successfully'.async() = > {const html = bigoMock.getMockData('githubIssues'.'html_mock.js'); / / file mock
  const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
  bigoMock.assert(result[0].title === 'Nginx Reverse Proxy implementation online Debugging');
  bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
});
Copy the code

3. The service relying on the mock

If another service method is called within a service method, we typically mock that service dependency to reduce the cost of coverage. For example, after the above crawler HTML parsing, the operation of database entry is needed. After this. Service. GithubIssues. Create a mock, this method is not implemented, direct return to the create. The json data, avoid the pollution test data warehousing.

it('HTML structure parsed successfully'.async() = > {const html = bigoMock.getMockData('githubIssues'.'html_mock.js'); / / file mock
  bigoMock.app.mockService("githubIssues"."create".'create.json'); // Service depends on mock

  const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
  bigoMock.assert(result[0].title === 'Nginx Reverse Proxy implementation online Debugging');
  bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
});
Copy the code

4. Context mocks

If we want to emulate the ctx.user data, we can also do so by passing the Data parameter to mockContext. Of course, I would recommend that services become less context-dependent and pass data through input parameters, avoiding the ctx.params.id script and making code testable.

it('should mock ctx.user'.() = > {
  const ctx = app.mockContext({
    user: {
      name: 'fengmk2',}}); assert(ctx.user); assert(ctx.user.name ==='fengmk2');
});
Copy the code

5. Single test database

Others use a single test database, where data is created at the beginning of the test and deleted at the end, using the before and after methods. In my opinion, the cost is high, and unit tests generally do not rely on other interfaces or systems, so mocks are fine.

Of course, the actual Service code is not as simple as the one in our example, and this is just how to test the Service. More scenes need to be supplemented.

Results the assertion

This has no silver bullet and is usually written with business logic.

/ / such as
result = {
  status: true.data: {
    age: '6'.name: 'bigo'.child: ['bigolive'.'likee'].}}// If it is a judgment state value, just write an assertion
bigoMock.assert(result.status === true);

// If it is a partial data exception, multiple assertions need to be combined
bigoMock.assert(result.status === true);
bigoMock.assert(result.data.name === 'bigo');
Copy the code

Write in the last

Testing is a means, not an end.

Software quality comes not from testing, but from design and maintenance.

read

For more details, please refer to eggjs.org/zh-cn/core/…

The sample unit test code for this article comes from: github.com/bigo-fronte…

Welcome everyone to leave a message to discuss, wish smooth work, happy life!

I’m bigO front. See you next time.