头图

React component unit testing missed in those years (part 1)

前端森林
中文

🏂 write in front

Regarding the front-end unit test, I actually paid attention to it two years ago, but at that time I simply knew the assertion, thinking that it was not too difficult, and it was not used in the project, and then I assumed that I would do it for granted. .

Today, two years later, the department needs to add unit tests to previous projects. When it was really time to start, but I was confused 😂

What I thought I thought I had pitted myself, and I found that I didn't know anything about front-end unit testing. Then I read a lot of documents, found on dva unit test documentation is relatively small, so after some practice with, I brushed a few articles, I hope to think use Jest were React + Dva + Antd unit testing you Can help. The content of the article strives to be in-depth and simple to understand~

Because the content is all contained in one article, it will be too long. The plan is divided into two articles. This article is the first one. It mainly introduces how to quickly get started with jest and the functions commonly used in actual combat and api

🏈 The background of front-end automated testing

Before introducing jest , I think it is necessary to briefly explain some basic information about front-end unit testing.

  • Why test?

    Today in 2021, it is not difficult for us to web Because there are enough excellent front-end frameworks (such as React , Vue ); and some easy-to-use and powerful UI libraries (such as Ant Design , Element UI ) to escort us, greatly shortening the cycle of application construction. However, the rapid iteration process has produced a lot of problems: low code quality (poor readability, low maintainability, low scalability), frequent product demand changes (uncontrollable range of code changes), etc.

    Therefore, the concept of unit testing came into being in the front-end field. By writing unit tests, you can ensure that you get the expected results and improve the readability of the code. If the dependent components are modified, the affected components can also find errors in the test in time.

  • What are the test types?

    There are generally the following four types:

    • unit test
    • function test
    • Integration Testing
    • Smoke test
  • What about common development models?

    • TDD : Test Driven Development
    • BDD : Behavior Driven Test

🎮 Technical solution

For the project itself, React + Dva + Antd is used, and the unit test we use is the combination of Jest + Enzyme

Jest

About Jest , we refer to it Jest official website , it is Facebook open source front end of a testing framework, mainly for React and React Native unit testing, it has been integrated in create-react-app in. Jest features:

  • Zero configuration
  • Snapshot
  • isolation
  • Excellent api
  • Fast and safe
  • Code coverage
  • Simulate easily
  • Excellent error message

Enzyme

Enzyme is the Airbnb open source React . It provides a set of simple and powerful API and built-in Cheerio . At the same time, it realizes the jQuery style method for DOM processing. The development experience is very friendly. React popular in the open source community and has also been officially recommended by 06076ab16bfd9d.

📌 Jest

In this article, we focus on introducing Jest , which is also the foundation of React unit test.

Environment setup

installation

Install Jest , Enzyme . If React version is 15 or 16 , you need to install the corresponding enzyme-adapter-react-15 and enzyme-adapter-react-16 and configuration.

/**
 * setup
 *
 */

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

jest.config.js

You can run npx jest --init to generate the configuration file jest.config.js

/*
 * 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\\.[^\\/]+$"],
}

Here is just a list of commonly used configuration items:

  • automock : Tell Jest that all modules are automatically imported from the mock.
  • clearMocks : Automatically clean up mock calls and instance instances before each test
  • collectCoverage : Whether to collect coverage information during testing
  • collectCoverageFrom : Coverage file detected when generating test coverage report
  • coverageDirectory : The directory where Jest outputs the coverage information file
  • coveragePathIgnorePatterns : List of files excluded from coverage
  • coverageReporters : List a list of reporter names, and Jest will use them to generate coverage reports
  • coverageThreshold : The threshold at which the test can pass
  • moduleDirectories : Module search path
  • moduleFileExtensions : Represents the file name that supports loading
  • testPathIgnorePatterns : Use regular to match files that do not need to be tested
  • setupFilesAfterEnv : configuration file, before running the test case code, Jest will first run the configuration file here to initialize the specified test environment
  • testMatch : Define the file to be tested
  • transformIgnorePatterns : Set which files do not need to be translated
  • transform : Set the codes in which files need to be converted into codes that Jest can recognize by the corresponding translator. Jest can recognize JS codes by default. Other languages, such as Typescript, CSS, etc., need to be translated.

Matcher

  • toBe(value) : Use Object.is to compare, if you compare floating-point numbers, use toBeCloseTo
  • not : Invert
  • toEqual(value) : Used for deep comparison of objects
  • toContain(item) : Used to judge whether the item is in an array, or it can be used to judge a string
  • toBeNull(value) : only match null
  • toBeUndefined(value) : only match undefined
  • toBeDefined(value) : Contrary to toBeUndefined
  • toBeTruthy(value) : match any sentence with a true value
  • toBeFalsy(value) : match any statement that is false
  • toBeGreaterThan(number) : greater than
  • toBeGreaterThanOrEqual(number) : greater than or equal to
  • toBeLessThan(number) : less than
  • toBeLessThanOrEqual(number) : less than or equal to
  • toBeInstanceOf(class) : Determine whether it is an instance of class
  • resolves : Used to retrieve the value of the package when the promise is fulfilled, support chain call
  • rejects : Used to retrieve the value of the package when the promise is rejected, and supports chained calls
  • toHaveBeenCalled() : used to determine whether the mock function has been called
  • toHaveBeenCalledTimes(number) : used to determine the number of times the mock function is called
  • assertions(number) : Verify that there are number assertions called in a test case

Use of command line tools

Add the following script 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",
},

npm run test at this time:

We found the following patterns:

  • f : Only test cases that have not passed before will be tested
  • o : Only the associated and changed files will be tested (git is required) (jest --watch can directly enter this mode)
  • p : Test case where the test file name contains the entered name
  • t : Test case whose name contains the entered name
  • a : Run all test cases

During the test, you can switch to the appropriate mode.

Hook function

Similar to the life cycle of react or vue, there are four types:

  • beforeAll() : The method to be executed before all test cases are executed
  • afterAll() : The method to be executed after all test cases are run
  • beforeEach() : The method that needs to be executed before each test case is executed
  • afterEach() : The method to be executed after each test case is executed

Here, I use a basic demo in the project to demonstrate the specific use:

Counter.js

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

Counter.test.js

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

test('测试 Counter 中的 addOne 方法', () => {
  counter.addOne()
  expect(counter.number).toBe(1)
})

test('测试 Counter 中的 minusOne 方法', () => {
  counter.minusOne()
  expect(counter.number).toBe(0)
})

Run npm run test :

By adding 1 to the first test case, number is 1. When the second case is subtracted by 1, the result should be 0. However, the interference between the two use cases is not good, which can be Jest by the hook function of 06076ab16c04ed. Modify the test case:

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('测试 Counter 中的 addOne 方法', () => {
  counter.addOne()
  expect(counter.number).toBe(1)
})
test('测试 Counter 中的 minusOne 方法', () => {
  counter.minusOne()
  expect(counter.number).toBe(-1)
})

Run npm run test :

You can clearly see the execution order of the corresponding hooks:

beforeAll> (beforeEach> afterEach) (a single use case will be executed in sequence)> afterAll

In addition to the above basic knowledge, there are actually asynchronous code testing, Mock, Snapshot snapshot testing, etc., which we will explain in turn in the following React unit test examples.

Asynchronous code testing

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

Under normal circumstances, the test code is executed synchronously, but when the code we want to test is asynchronous, there will be a problem: test case actually ended, but our asynchronous code has not been executed yet, resulting in the asynchronous code not being tested To.

then what should we do?

For the current test code, the asynchronous code does not know when to execute it, so the solution is simple. When there is asynchronous code, the test code does not end immediately after running the synchronous code, but waits for the end notification. When the asynchronous code is executed, tell jest : "Okay, the asynchronous code is executed, you can end the task" .

jest provides three solutions to test asynchronous code, let's take a look at them separately.

done keyword

When an asynchronous callback function appears in test function, we can pass in a done test function, which is a function type parameter. If the test function is passed in done , jest will wait until done is called before ending the current test case . If done not called, the test will automatically fail the test.

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

The above code, we have to test function was introduced to done parameters, in fetchData call a callback function done . In this way, the test code executed asynchronously in the callback of fetchData

But here we think about a scenario: if you use done to test the callback function (including the timer scenario, such as setTimeout ), because the timer we set a certain delay (such as 3s) and execute it, we will find that the test passes after waiting for 3s. . So if setTimeout set to a few hundred seconds, should we also test after a few hundred seconds Jest

Obviously, this greatly reduces the efficiency of the test! !

jest jest.useFakeTimers() such as 06076ab16c0687, jest.runAllTimers() and toHaveBeenCalledTimes , jest.advanceTimersByTime etc. api to deal with this kind of scene.

I will not give an example here in detail, students who have this need can refer to Timer Mocks

Return Promise

⚠️ When Promise , be sure to add a return before the assertion, otherwise the test function will end Promise You can use .promises/.rejects to get the returned value, or use the then/catch method for judgment.

If the code used in Promise , it can return Promise to handle asynchronous code, jest will wait for the promise state into resolve will end, if promise is reject , and does not pass the test.

// 假设 user.getUserById(参数id) 返回一个promise
it('测试promise成功的情况', () => {
  expect.assertions(1);
  return user.getUserById(4).then((data) => {
    expect(data).toEqual('Cosen');
  });
});
it('测试promise错误的情况', () => {
  expect.assertions(1);
  return user.getUserById(2).catch((e) => {
    expect(e).toEqual({
      error: 'id为2的用户不存在',
    });
  });
});

Note that the second test case above can be used to test the situation where promise returns reject Here we use .catch to capture the promise returned by reject . When promise returns reject expect statement will be executed. expect.assertions(1) here is used to ensure that a expect is executed in the test case.

For Promise case, jest also provides a pair of matching symbols resolves/rejects , in fact, just syntactic sugar written above. The above code can be rewritten as:

// 使用'.resolves'来测试promise成功时返回的值
it('使用'.resolves'来测试promise成功的情况', () => {
  return expect(user.getUserById(4)).resolves.toEqual('Cosen');
});
// 使用'.rejects'来测试promise失败时返回的值
it('使用'.rejects'来测试promise失败的情况', () => {
  expect.assertions(1);
  return expect(user.getUserById(2)).rejects.toEqual({
    error: 'id为2的用户不存在',
  });
});

async/await

We know that async/await is Promise the syntactic sugar of 06076ab16c07e3, which can be used to write asynchronous code more elegantly. This kind of syntax is also supported in jest

Let's rewrite the above code:

// 使用async/await来测试resolve
it('async/await来测试resolve', async () => {
  expect.assertions(1);
  const data = await user.getUserById(4);
  return expect(data).toEqual('Cosen');
});
// 使用async/await来测试reject
it('async/await来测试reject', async () => {
  expect.assertions(1);
  try {
    await user.getUserById(2);
  } catch (e) {
    expect(e).toEqual({
      error: 'id为2的用户不存在',
    });
  }
});
⚠️ Use async without return , and use try/catch to catch exceptions.

Mock

Before introducing jest in mock , let's first think about a question: Why use the mock function?

In a project, a method of a module often calls a method of another module. In unit testing, we may not need to care about the execution process and results of the internally called method, just want to know whether it is called correctly, and even specify the return value of the function. At this time, mock is of great significance.

jest are mainly three mock related to api jest.fn() , jest.mock() , jest.spyOn() . Using them to create the mock function can help us better test some logically complex codes in the project. In our test, we mainly used the following three features provided by mock

  • Capturing function calls
  • Set function return value
  • Change the internal implementation of the function

Below, I will introduce these three methods and their application in actual testing.

jest.fn()

jest.fn() is the easiest way to create the mock function. If the internal implementation of the function is not defined, jest.fn() will return undefined as the return value.

// functions.test.js

test('测试jest.fn()调用', () => {
  let mockFn = jest.fn();
  let res = mockFn('厦门','青岛','三亚');

  // 断言mockFn的执行后返回undefined
  expect(res).toBeUndefined();
  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  // 断言mockFn被调用了一次
  expect(mockFn).toBeCalledTimes(1);
  // 断言mockFn传入的参数为1, 2, 3
  expect(mockFn).toHaveBeenCalledWith('厦门','青岛','三亚');
})

jest.fn() created mock function return values may also be provided, define internal implementation or return Promise objects.

// functions.test.js

test('测试jest.fn()返回固定值', () => {
  let mockFn = jest.fn().mockReturnValue('default');
  // 断言mockFn执行后返回值为default
  expect(mockFn()).toBe('default');
})

test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 + num2;
  })
  // 断言mockFn执行后返回20
  expect(mockFn(10, 10)).toBe(20);
})

test('测试jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('default');
  let res = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为default
  expect(res).toBe('default');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})

jest.mock()

Generally, in real projects, when testing asynchronous functions, the ajax request will not be sent to request this interface. Why?

For example, there are 1w interfaces to be tested, and each interface requires 3s to return. To test all interfaces, 30000s required, so the time for this automated test is too slow

As the front-end, we only need to confirm that the asynchronous request is sent successfully. As for the content returned by the back-end interface, we will not test it. This is what the back-end automated test needs to do.

Here is an 06076ab16c09ed requested by demo :

// user.js
import axios from 'axios'

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

Corresponding test file user.test.js :

import { getUserList } from '@/services/user.js'
import axios from 'axios'
// 👇👇
jest.mock('axios')
// 👆👆
test.only('测试 getUserList', async () => {
  axios.get.mockResolvedValue({ data: ['Cosen','森林','柯森'] })
  await getUserList().then(data => {
    expect(data).toBe(['Cosen','森林','柯森'])
  })
})

jest.mock('axios') the top of the test case, and we let jest axios so that it would not request real data. Then axios.get is called, it will not actually request this interface, but will use our {data: ['Cosen','Forest','Cosen']} to simulate the result of the successful request.

Of course, it takes time to simulate asynchronous requests. If there are many requests, it will take a long time. At this time, you can mock data in the local 06076ab16c0a83 and create a new folder __mocks__ This method does not need to simulate axios , but a direct local simulation method, which is also a more commonly used method, which will not be explained here.

jest.spyOn()

jest.spyOn() method also creates a mock function, but the mock function can not only capture the function call, but also execute the function spy In fact, jest.spyOn() is jest.fn() syntactic sugar, and it creates a being spy function with the same internal code mock function.

Snapshot test

The so-called snapshot is also a snapshot. Usually involving automated testing of UI, the idea is to take a snapshot of the standard state at a certain moment.

describe("xxx页面", () => {
  // beforeEach(() => {
  //   jest.resetAllMocks()
  // })
  // 使用 snapshot 进行 UI 测试
  it("页面应能正常渲染", () => {
    const wrapper = wrappedShallow()
    expect(wrapper).toMatchSnapshot()
  })
})

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 with the old snapshot. If the two are inconsistent, the test will fail, which helps us ensure that the user interface does not change unexpectedly.

🎯 Summary

Here, some basic background on the front of unit testing and Jest based api will introduce over the next article, I will combine a project React component to explain how to do component unit testing.

📜 Reference link

阅读 2.1k

前端森林公众号
一个有温度的前端号,关注行业前沿。从基础到架构,携手你我共同成长。
1.9k 声望
11.3k 粉丝
0 条评论
你知道吗?

1.9k 声望
11.3k 粉丝
宣传栏