🏂 comes first

In terms of front-end unit testing, I’ve been paying attention to it since two years ago, but at that time I simply knew assertions, thought about things that weren’t too difficult, didn’t use them in my projects, and took it for granted that I could do it.

Now, two years later, the department is adding unit tests to previous projects. Really arrived at the beginning of the time, but meng 😂

What I thought I thought I screwed myself up by discovering that I knew nothing about front-end unit testing. Then I went through a lot of documents, found that dVA-BASED unit test documents are very few, so after some practice, I sorted out a few articles, hoping to help you who want to use Jest for React + DVA + Antd unit test. The content of the article strives to be simple and easy to understand

Since it would be too long to contain all the contents in one article, we plan to divide it into two parts. This is the first part, which mainly introduces how to quickly get started with JEST and the functions and APIS commonly used in the actual practice

🏈 Background to front-end automation test generation

Before getting started with JEST, I thought it would be worth briefly explaining some basic information about front-end unit testing.

  • Why test?

    In 2021, it’s not that hard to build a complex Web application. There are plenty of good front-end frameworks (React, Vue); Easy to use and powerful UI libraries such as Ant Design and Element UI have helped us greatly reduce the application build cycle. However, in the process of rapid iteration, a large number of problems were generated: low code quality (poor readability, low maintainability, low scalability), frequent changes in product requirements (the influence range of code changes is uncontrollable), etc.

    Therefore, the concept of unit testing emerged in the front-end domain. By writing unit tests, we can ensure the expected results, improve the readability of code, and if the dependent components change, the affected components can detect errors in the test in time.

  • What are the types of tests?

    Generally, there are four kinds of common:

    • Unit testing
    • A functional test
    • Integration testing
    • Smoke test
  • What are common development patterns?

    • TDD: Test-driven development
    • BDD: Behavior driven testing

🎮 Technical Solution

React + Dva + Antd technology stack is used for the project itself, and Jest + Enzyme method is used for unit test.

Jest

Jest is Facebook’s open source front-end testing framework for React and React Native unit testing, which is integrated into create-React-app. Jest features:

  • Zero configuration
  • The snapshot
  • isolation
  • Excellent API
  • Fast and safe
  • Code coverage
  • Easy to simulate
  • Good error message

Enzyme

Enzyme is Airbnb’s open source React test tool library, which provides a simple and powerful API with built-in Cheerio. Meanwhile, DOM processing is implemented in a jquery-style way, making the development experience very friendly. It has gained popularity in the open source community and was officially featured by React.

📌 Jest

This article focuses on Jest, which is the foundation for our entire React unit test.

Environment set up

The installation

Install Jest, Enzyme. If the React version is 15 or 16, install the corresponding enzyme- Adapter-react -15 and enzyme- Adapter-react -16 and configure them.

/** * setup * */

import Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"
Enzyme.configure({ adapter: new Adapter() })
Copy the code

jest.config.js

You can run NPX jest –init to generate the configuration file jest.config.js in the root directory

/*
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/en/configuration.html
 */

module.exports = {
  // All imported modules in your tests should be mocked automatically
  // automock: false,

  // Automatically clear mock calls and instances between every test
  clearMocks: true,

  // Indicates whether the coverage information should be collected while executing the test
  // collectCoverage: true,

  // An array of glob patterns indicating a set of files for which coverage information should be collected
  collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // An array of regexp pattern strings used to skip coverage collection
  // coveragePathIgnorePatterns: [
  //   "/node_modules/"
  // ],


  // An array of directory names to be searched recursively up from the requiring module's location
  moduleDirectories: ["node_modules", "src"],

  // An array of file extensions your modules use
  moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx"],


  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
  // modulePathIgnorePatterns: [],

  // Automatically reset mock state between every test
  // resetMocks: false,

  // Reset the module registry before running each individual test
  // resetModules: false,

  // Automatically restore mock state between every test
  // restoreMocks: false,

  // The root directory that Jest should scan for tests and modules within
  // rootDir: undefined,

  // A list of paths to directories that Jest should use to search for files in
  // roots: [
  //   "<rootDir>"
  // ],

  // The paths to modules that run some code to configure or set up the testing environment before each test
  // setupFiles: [],

  // A list of paths to modules that run some code to configure or set up the testing framework before each test
  setupFilesAfterEnv: [
    "./node_modules/jest-enzyme/lib/index.js",
    "<rootDir>/src/utils/testSetup.js",
  ],

  // The test environment that will be used for testing
  testEnvironment: "jest-environment-jsdom",

  // Options that will be passed to the testEnvironment
  // testEnvironmentOptions: {},

  // The glob patterns Jest uses to detect test files
  testMatch: ["**/?(*.)+(spec|test).[tj]s?(x)"],

  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
  // testPathIgnorePatterns: [
  //   "/node_modules/"
  // ],


  // A map from regular expressions to paths to transformers
  // transform: undefined,

  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
  transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\/]+$"],
}

Copy the code

Common configuration items are listed here:

  • automock: tells Jest that all modules are automatically imported from the mock.
  • clearMocks: Automatically cleans up mock calls and instances before each test
  • collectCoverage: Whether to collect coverage information at test time
  • collectCoverageFrom: The coverage file tested when generating the test coverage report
  • coverageDirectory: Jest directory for output overwrite information files
  • coveragePathIgnorePatterns: Exclude coverage’s file list
  • coverageReporters: Lists lists of reporter names that Jest uses to generate coverage reports
  • coverageThreshold: Indicates the threshold for the test to pass
  • moduleDirectories: Module search path
  • moduleFileExtensions: indicates the file name that can be loaded
  • testPathIgnorePatterns: Uses re to match files that are not being tested
  • setupFilesAfterEnv: configuration file, which Jest runs to initialize the specified test environment before running the test case code
  • testMatch: Defines the file to be tested
  • transformIgnorePatterns: Sets which files do not need to be translated
  • transformJest recognizes JAVASCRIPT code by default. Other languages, such as Typescript, CSS, etc., need to be translated.

matcher

  • toBe(value): Use object. is for comparison, or toBeCloseTo for floating-point comparisons
  • not: take the
  • toEqual(value): used for deep comparisons of objects
  • toContain(item): is used to check whether an item is in an array. It can also be used to check strings
  • toBeNull(value): Matches only null
  • toBeUndefined(value): Matches only undefined
  • toBeDefined(value): The opposite of toBeUndefined
  • toBeTruthy(value): matches any statement as true
  • toBeFalsy(value): Matches any value whose statement is false
  • toBeGreaterThan(number): more than
  • toBeGreaterThanOrEqual(number): Greater than or equal to
  • toBeLessThan(number): less than
  • toBeLessThanOrEqual(number): Less than or equal to
  • toBeInstanceOf(class): Determines whether it is an instance of class
  • resolves: Is used to take out the value of the package when the promise is fulfilled, which supports chain call
  • rejects: fetches the value of the package rejected when promise is rejected
  • toHaveBeenCalled(): used to determine whether the mock function has been called
  • toHaveBeenCalledTimes(number): determines how many times the mock function is called
  • assertions(number)Verify that there are a number of assertions called in a test case

Use of command line tools

Add the following script to the project package.json file:

"scripts": {
    "start": "node bin/server.js",
    "dev": "node bin/server.js",
    "build": "node bin/build.js",
    "publish": "node bin/publish.js",
++ "test": "jest --watchAll",
},
Copy the code

At this time to runnpm run test:

We found the following patterns:

  • f: Tests only test cases that have not previously passed
  • o: tests only associated and changed files (using git) (jest –watch can enter this mode directly)
  • p: Test file name contains the test case of the entered name
  • t: Test case name Test case that contains the entered name
  • a: Runs all test cases

During testing, you can switch between the appropriate modes.

Hook function

The React or Vue life cycle has four types:

  • beforeAll(): Methods executed before all test cases are executed
  • afterAll(): Method to execute after all test cases have run
  • beforeEach(): Methods that need to be executed before each test case is executed
  • afterEach(): Method to execute after each test case is executed

Here, I use a basic demo from the project to show how to use it:

Counter.js

export default class Counter { constructor() { this.number = 0 } addOne() { this.number += 1 } minusOne() { this.number - = 1}}Copy the code

Counter.test.js

import Counter from './Counter'
const counter = new Counter()

test('Test addOne method in Counter'.() = > {
  counter.addOne()
  expect(counter.number).toBe(1)
})

test('Test the minusOne method in Counter'.() = > {
  counter.minusOne()
  expect(counter.number).toBe(0)})Copy the code

runnpm run test:

By incrementing the first test case by 1, the value of number is 1. When the second test case is subtracted by 1, the result should be 0. However, such two use cases do not interfere with each other, which can be solved by Jest’s hook function. Modify test cases:

import Counter from ".. /.. /.. /src/utils/Counter";
let counter = null

beforeAll(() = > {
  console.log('BeforeAll')
})

beforeEach(() = > {
  console.log('BeforeEach')
  counter = new Counter()
})

afterEach(() = > {
  console.log('AfterEach')
})

afterAll(() = > {
  console.log('AfterAll')
})

test('Test addOne method in Counter'.() = > {
  counter.addOne()
  expect(counter.number).toBe(1)
})
test('Test the minusOne method in Counter'.() = > {
  counter.minusOne()
  expect(counter.number).toBe(-1)})Copy the code

runnpm run test:

You can clearly see the sequence of hooks:

BeforeAll > (beforeEach > afterEach)(individual use cases are executed sequentially) > afterAll

In addition to these basics, there are also asynchronous code testing, mocks, and Snapshot testing, which we’ll cover in the unit test example on React.

Testing asynchronous code

As we all know, JS is full of asynchronous code.

Normally the test code is executed synchronously, but when the code we are testing is asynchronous, there is a problem: the test case has actually ended, but our asynchronous code has not yet been executed, resulting in the asynchronous code not being tested.

So what to do?

It is not known to the currently tested code when the asynchronous code executes it, so the solution is simple. When there is asynchronous code, the test code does not finish immediately after the synchronous code runs. Instead, the test code waits for the completion notification, and then tells jest when the asynchronous code is finished: “Ok, the asynchronous code is finished, you can finish the task.”

Jest provides three ways to test asynchronous code, and let’s take a look at each.

Done a keyword

When an asynchronous callback occurs in our test function, we can pass a done argument to the test function, which is a function type argument. If the test function passes done, jest will wait until done is called before terminating the current test case. If done is not called, the test will automatically fail.

import { fetchData } from './fetchData'
test('fetchData 返回结果为 { success: true }'.done= > {
  fetchData(data= > {
    expect(data).toEqual({
      success: true
    })
    done()
  })
})
Copy the code

In the above code, we pass the done argument to the test function and call done in the fetchData callback. In this way, the test code that is executed asynchronously in fetchData’s callback can be executed.

But here we consider a scenario: if we use done to test the callback function (including timer scenarios, such as setTimeout), we set a certain delay (such as 3s) after the timer is executed, and wait for 3s to find that the test passes. If setTimeout is set to a few hundred seconds, should we wait a few hundred seconds before testing Jest?

Obviously, this is very inefficient for testing!!

Apis such as jest.usefaketimers (), jest.RunallTimers () and toHaveBeenCalledTimes, jest.AdvanceTimersbyTime are provided in Jest to handle this scenario.

I’m not going to give you an example here, but if you need one, you can refer to Timer Mocks

Return to the Promise

⚠️ When testing a Promise, be sure to precede the assertion with a return, or the test function will end without waiting for the Promise’s return. You can use.promises/.rejects to obtain the returned value, or use the then/catch method to judge.

If a Promise is used in the code, the asynchronous code can be processed by returning a Promise, and jEST will wait until the state of the Promise changes to resolve. If the Promise is rejected, the test case fails.

// Suppose user.getUserById (parameter id) returns a Promise it(' Testing the promise success ', () => {expect. Assertions (1); return user.getUserById(4).then((data) => { expect(data).toEqual('Cosen'); }); }); It (' Test promise error ', () => {expect. Assertions (1); Return user.getUserById(2). Catch ((e) => {expect(e).toequal ({error: 'there is no user with id 2 ',}); }); });Copy the code

Note that the second test case above can be used to test the case where a PROMISE returns REJECT. A. Catch is used to catch the reject returned by the promise, and when the promise returns reject, the Expect statement is executed. Here, expect. Assertions (1) are used to ensure that one of expect is executed in this test case.

In the case of Promise, Jest also offers a pair of high similarity /rejects, which is really just a grammatical sugar from the above notation. The above code can be rewritten as:

// Use '.convergency 'to test the value returned when the promise succeeds
it('use'.resolves'To test the promise's success'.() = > {
  return expect(user.getUserById(4)).resolves.toEqual('Cosen');
});
// Use '.rejects' to test the value returned when a promise fails
it('use'.rejects'To test if a promise fails.'.() = > {
  expect.assertions(1);
  return expect(user.getUserById(2)).rejects.toEqual({
    error: 'User id 2 does not exist'}); });Copy the code

async/await

We know that async/await is a Promise syntactic sugar for writing asynchronous code more elegantly, and jest also supports this syntax.

Let’s rewrite the above code:

// Test resolve with async/await
it('async/await to test resolve'.async () => {
  expect.assertions(1);
  const data = await user.getUserById(4);
  return expect(data).toEqual('Cosen');
});
// Test async/await with reject
it('async/await to test reject'.async () => {
  expect.assertions(1);
  try {
    await user.getUserById(2);
  } catch (e) {
    expect(e).toEqual({
      error: 'User id 2 does not exist'}); }});Copy the code

⚠️ async does not return and uses try/catch to catch exceptions.

Mock

Before we get to mocks in Jest, let’s consider the following question: Why use mock functions?

In a project, it is common for methods in one module to call methods in another module. In unit tests, we might not care about the execution and results of an internally called method, just whether it was called correctly, and even specify the return value of that function. This is where mock comes in.

There are three main mock apis in Jest: jest.fn(), jest.mock(), and jest.spyon (). Using them to create mock functions can help us better test some of the more logically complex code in our projects. In our tests, we mainly used the following three features provided by mock functions:

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

Below, I’ll describe each of these methods and their application to real-world testing.

jest.fn()

Jest.fn () is the easiest way to create mock functions, and if the internal implementation of the function is not defined, jest.fn() returns undefined as the return value.

// functions.test.js

test('Test the jest. Fn () call'.() = > {
  let mockFn = jest.fn();
  let res = mockFn('xiamen'.'Qingdao'.'sanya');

  // Assert that execution of mockFn returns undefined
  expect(res).toBeUndefined();
  // Assert mockFn is called
  expect(mockFn).toBeCalled();
  // Assert that mockFn is called once
  expect(mockFn).toBeCalledTimes(1);
  // Assert that mockFn passes in arguments 1, 2, 3
  expect(mockFn).toHaveBeenCalledWith('xiamen'.'Qingdao'.'sanya');
})
Copy the code

The mock function created by jest.fn() can also set return values, define internal implementations, or return Promise objects.

// functions.test.js

test('Test jest.fn() returns fixed value'.() = > {
  let mockFn = jest.fn().mockReturnValue('default');
  Assert that mockFn returns a value of default after execution
  expect(mockFn()).toBe('default');
})

test('Test the internal implementation of jest.fn()'.() = > {
  let mockFn = jest.fn((num1, num2) = > {
    return num1 + num2;
  })
  Return 20 after asserting mockFn execution
  expect(mockFn(10.10)).toBe(20);
})

test('Test jest.fn() returns Promise'.async() = > {let mockFn = jest.fn().mockResolvedValue('default');
  let res = await mockFn();
  // assert mockFn returns default after execution with await keyword
  expect(res).toBe('default');
  // Assert that a mockFn call returns a Promise object
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
Copy the code

jest.mock()

In real projects, when testing asynchronous functions, you don’t actually send ajax requests to this interface. Why?

For example, 1W interfaces need to be tested, each interface needs 3s to return, and testing all interfaces needs 30000s, so the time of this automated test is too slow

We, as the front end, just need to confirm that the asynchronous request has been sent successfully, and we are in trouble as to what the back end interface returns, which is what the back end automation tests do.

Here’s a demo of an AXIos request:

// user.js
import axios from 'axios'

export const getUserList = () = > {
  return axios.get('/users').then(res= > res.data)
}

Copy the code

Corresponding test file user.test.js:

import { getUserList } from '@/services/user.js'
import axios from 'axios'
/ / 👇 👇
jest.mock('axios')
/ / 👆 👆
test.only('test getUserList'.async () => {
  axios.get.mockResolvedValue({ data: ['Cosen'.'forest'.'KeSen']})await getUserList().then(data= > {
    expect(data).toBe(['Cosen'.'forest'.'KeSen'])})})Copy the code

Mock (‘axios’) we added jest. Mock (‘axios’) at the top of our test case. We told Jest to mock axios so that we didn’t ask for real data. When we call axios.get, instead of actually requesting the interface, we’ll simulate the result of a successful request with {data: [‘Cosen’,’ forest ‘,’ Cursen ‘]}.

Of course, it takes time to simulate asynchronous requests, which can be very long if there are many requests. In this case, you can create a __mocks__ folder in the root directory of your mock data. This way, instead of emulating Axios, you go directly to the native simulation method, which is more commonly used and won’t be explained here.

jest.spyOn()

The jest.spyon () method also creates a mock function, but the mock function not only captures the function invocation, but also executes the spy function normally. In fact, jest.spyon () is the syntactic sugar of jest.fn(), which creates a mock function with the same internal code as the spy function.

Snapshot Snapshot test

A snapshot is a snapshot. This usually involves automated testing of the UI, and the idea is to take a snapshot of the standard state at any given moment.

describe("XXX page".() = > {
  // beforeEach(() => {
  // jest.resetAllMocks()
  // })
  // Use snapshot for UI testing
  it("The page should render normally.".() = > {
    const wrapper = wrappedShallow()
    expect(wrapper).toMatchSnapshot()
  })
})
Copy the code

When using toMatchSnapshot, Jest will render the component and create its snapshot file. This snapshot file contains the entire structure of the rendered component and should be submitted to the code base along with the test file itself. When we run the snapshot test again, Jest will compare the new snapshot to the old one, and if the two are inconsistent, the test will fail, helping us ensure that the user interface does not change unexpectedly.

🎯 summary

That concludes some of the basic background on front-end unit testing and the basic APIS of Jest. In the next article, I’ll explain how to do component unit testing with a React component in my project.

📜 Reference link

  • Segmentfault.com/a/119000001…