The first attempt to learn the knowledge through the way of writing down. In the process of writing articles, I found myself more shortcomings and the importance of testing. This article mainly records the basic knowledge and environment construction needed for unit test of React technology stack using Jest + Enzyme.

The term is commonly used

  • According to the means of testing
    • Black box: Completely ignore the internal structure and working process of the program, only concerned about whether the function of the program is normal.
    • White box: Know the internal logic structure of the program, and test it to prove that the program can work correctly according to the predetermined requirements.
    • Grey box: A test that falls between black and white boxes, focusing on output versus input correctness as well as internal performance, but not as detailed and complete as white boxes.
  • According to the special division
    • function: Tests whether the program meets the user’s surface requirements.
    • performance: Tests the efficiency of the program.
    • security: Test whether the program can protect the user’s information and ensure that the information is not easily stolen.
  • Divided according to test points
    • compatibility: Tests how the application performs on different platforms.
    • Ease of use: Test whether the program is friendly, to meet the user’s use habits.
    • The UI elements: The page layout is consistent and beautiful.

Why test

  • As a description of the behavior of existing code.
  • Improve project robustness and reliability.
  • Reduce risk associated with iterative refactoring of projects.
  • Encourage developers to write testable code.
  • If a dependent component is modified, the affected component can detect errors in the test.
  • Reduce the cost of manpower testing and improve the efficiency of testing.
  • .

Front-end test pyramid

There are many testing frameworks, assertion libraries (chai, Expect. Js, should. Js, sinon.js, etc.), and tools.

  • 1. Unit Tests

    • Jest
    • mocha
    • jasmine
  • 2. Snapshot Tests

    • Jest
  • 3. E2e Tests

    • Puppeteer
    • Selenium

React unit test

  • Technology selection

    • Jest

      • Jest is Facebook’s open source front-end testing framework with built-in JSDOM, snapshots, Mock functionality, and test code coverage.
    • Enzyme

      • Enzyme is Airbnb’s open source React test tool. It encapsulates the official test tool library and operates dom in a way that mimics jQuery. It can easily assert, manipulate, and iterate the output of React Components.
  • Environment set up

    • Install the Jest

      npm install --save-dev jest
      Copy the code
    • Install the Enzyme

      NPM install --save-dev enzyme jest-enzyme // react https://airbnb.io/enzyme/ NPM install - save - dev enzyme - adapter - react - 16 / / if you are using version 16.4 and above the react, You can also set jest's environment NPM install --save-dev jest-environment-enzyme by installing jest-environment-enzymeCopy the code
    • Install Babel

      npm install --save-dev babel-jest babel-core npm install --save-dev babel-preset-env npm install --save-dev Babel-preset -react // stage-0 NPM install --save-dev babel-preset-stage-0 // Load the plugin NPM install --save-dev as required babel-plugin-transform-runtimeCopy the code
    • Modify the package. The json

      // package.json
      {
        "scripts": {
          "test": "jest"}}Copy the code
    • Install other libraries as needed

      Dom NPM install --save jqueryCopy the code
    • Jest configuration

      For more information about Jest configuration see jestjs. IO /docs/ zh-han…

      // jest.config.js
      module.exports = {
        setupFiles: ['./jest/setup.js'], // configure the test environment, where these scripts will be executed immediately before executing the test code itself. setupTestFrameworkScriptFile:'jest-enzyme'// Configure the test frameworktestEnvironment: 'enzyme', // The configuration required to use the jest-environment-enzymetestEnvironmentOptions: {
          enzymeAdapter: 'react16'// React adapter version},testPathIgnorePatterns: ['<rootDir>/node_modules/'.'<rootDir>/src/'], // Ignored directory transform: {// compile configuration'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest'.'^.+\\.(css|scss)$': '<rootDir>/jest/cssTransform.js'.'^ (? ! .*\\.(js|jsx|ts|tsx|css|json)$)': '<rootDir>/jest/fileTransform.js',}};Copy the code
    • Test a Function using Jest

      // add.js
      const add = (a, b) => a + b;
      export default add;
      Copy the code
      // __tests__/add-test.js
      import add from '.. /add';
      
      describe('add () test:', () => {
        it('1 + 2 = 3', () => { expect(add(1, 2)).toBe(3); // The assertion passes, but what if we pass a string? }); });Copy the code
      // Execute Jest NPMtest// or jest add-test.js --verboseCopy the code
    • A snapshot of the test

      Snapshot tests can be a useful tool if you want to make sure your UI doesn’t accidentally change.

      NPM install --save react react-dom react-test-rendererCopy the code
      // components/Banner.js
      import React from 'react';
      
      const Banner = ({ src }) => (
        <div>
          <img src={src} alt="banner" />
        </div>
      );
      
      export default Banner;
      Copy the code
      // __tests__/components/Banner-test.js
      import React from 'react';
      import renderer from 'react-test-renderer';
      import Banner from '.. /.. /components/Banner';
      
      describe('<Banner />', () => {
        it('renders correctly', () => {
          const tree = renderer.create(<Banner />).toJSON();
          expect(tree).toMatchSnapshot();
        });
      });
      Copy the code
    • JSDOM (Headless browser implemented by JS)

      The most powerful capability of JsDOM is that it can execute scripts in jsDOM. These scripts can modify page content and access all of the Web platform apis implemented by JsDOM.

      // handleBtn.js
      const $ = require('jquery');
      
      $('#btn').click(() => $('#text').text('click on the button'));
      Copy the code
      // handleBtn-test.js
      describe('JSDOM test', () => {
        it('click on the button', () => {
          // initialization document
          document.body.innerHTML = '<div id="btn"><span id="text"></span></div>';
      
          const $ = require('jquery');
      
          require('.. /handleBtn');
      
          // simulation button click
          $('#btn').click();
      
          // the text is updated as expected
          expect($('#text').text()).toEqual('click on the button');
        });
      });
      Copy the code
    • The Mock module

      Create a new __mocks__ directory in the module directory where you Mock, create the same file name, and add jest. Mock (‘.. /moduleName’) to Mock the module.

      // request.js
      const http = require('http');
      
      export default function request(url) {
        returnNew Promise(resolve => {// This is an example of an HTTP request to get user information from the API // This module is being mockedin __mocks__/request.js
          http.get({ path: url }, response => {
            let data = ' ';
            response.on('data', _data => {
              data += _data;
            });
            response.on('end', () => resolve(data));
          });
        });
      }
      
      Copy the code
      // __mocks__/request.js
      const users = {
        4: { name: 'Mark' },
        5: { name: 'Paul'}};export default function request(url) {
        return new Promise((resolve, reject) => {
          const userID = parseInt(url.substr('/users/'.length), 10);
          process.nextTick(() => (users[userID] ? resolve(users[userID]) : reject(new Error(`User with ${userID} not found.`))));
        });
      }
      Copy the code
      // __tests__/request.js
      jest.mock('.. /request.js');
      import request from '.. /request';
      
      describe('mock request.js', () => {
        it('works with async/await', async () => { expect.assertions(2); Const res = await request() const res = await request('/users/4');
          expect(res).toEqual({ name: 'Mark'}); // Error return the assertion await expect(request('/users/41')).rejects.toThrow('User with 41 not found.');
        });
      });
      Copy the code
    • Test component node

      • shallow: Shallow rendering, testing the component as a unit, and ensuring that your tests do not indirectly assert the behavior of the child components. Supports interaction simulation and function testing within components
      • renderRender the React component as a static HTML string. Then use the Cheerio library to parse the string and return an instance of Cheerio that can be used to analyze the HTML structure of the component. Can be used to determine child components.
      • mount: Full rendering, full DOM rendering is ideal for use cases where you have components that might interact with THE DOM API or need to test that are contained in higher-order components. Rely onjsdomThe library is essentially a headless browser implemented entirely in JS. Supports interaction simulation and function testing within components
      // components/List.js
      import React, { Component } from 'react';
      
      export default class List extends Component {
        constructor(props) {
          super(props);
      
          this.state = {
            list: [1],
          };
        }
      
        render() {
          const { list } = this.state;
          return( <div> {list.map(item => ( <p key={item}>{item}</p> ))} </div> ); }}Copy the code
      // __tests__/components/List-test.js
      import React from 'react';
      import { shallow, render, mount } from 'enzyme';
      import List from '.. /.. /components/List';
      
      describe('<List />', () => {
        it(Shallow: render 
                 component', () => {
          const wrapper = shallow(<List />);
          expect(wrapper.find('div').length).toBe(1);
        });
      
        it('Render: render 
                 component', () => {
          const wrapper = render(<List />);
          expect(wrapper.html()).toBe('<p>1</p>');
        });
      
        it('Mount: allows us to setState', () => {
          const wrapper = mount(<List />);
          wrapper.setState({
            list: [1, 2, 3],
          });
          expect(wrapper.find('p').length).toBe(3);
        });
      });
      Copy the code
    • Test component internal functions

      // components/TodoList.js
      import React, { Component } from 'react';
      
      export default class TodoList extends Component {
        constructor(props) {
          super(props);
      
          this.state = {
            list: [],
          };
        }
      
        handleBtn = () => {
          const { list } = this.state;
          this.setState({
            list: list.length ? [...list, list.length] : [0],
          });
        };
      
        render() {
          const { list } = this.state;
          return (
            <div>
              {list.map(item => (
                <p key={item}>{item}</p>
              ))}
              <button type="button"onClick={() => this.handleBtn}> add item </button> </div> ); }}Copy the code
      // __tests__/components/TodoList-test.js
      import React from 'react';
      import { shallow } from 'enzyme';
      import TodoList from '.. /.. /components/TodoList';
      
      describe('<TodoList />', () => {
        it('calls component handleBtn', () => { const wrapper = shallow(<TodoList />); Const spyHandleBtn = jest.spyon (wrapper.instance(),'handleBtn'); // List defaults to 0 expect(wrapper.state()'list').length).toBe(0); // first handelBtn wrapper.instance().handlebtn (); expect(wrapper.state('list').length).toBe(1); // Mock the wrapper.find('button').simulate('click');
          expect(wrapper.state('list').length).toBe(2);
      
          // 总共执行handleBtn函数两次
          expect(spyHandleBtn).toHaveBeenCalledTimes(2);
      
          // 恢复mockFn
          spyHandleBtn.mockRestore();
        });
      });
      Copy the code
    • Test code coverage

      • Statement Coverage: Whether each statement of the test case is executed
      • Branch coverage: Whether each if block of the test case is executed
      • Function coverage: Whether every function of the test case is called
      • Line coverage: Whether every line of the test case is executed
      // jest.config.js
      module.exports = {
        collectCoverage: trueCoverageThreshold: {// set the minimum coverageThreshold of global: {branches: 50,functions: 50,
            lines: 50,
            statements: 50,
          },
          './firstTest/components': {
            branches: 100,
          },
        },
      };
      Copy the code

Redux unit tests

  • The installation

    • Redux-thunk: A library for managing Redux Side effects, such as retrieving data asynchronously
    • Redux-saga: Redux side effects management is easier, execution is more efficient, testing is simpler, and troubleshooting is easier.
    • Fetch -mock: mocks the fetch request
    • Node-fetch: fetch-mock depends on node-fetch
    • Redux-mock-store: mock store used to test the Redux asynchronous operation creator and middleware. Primarily used to test operation-related logic, not reduce-related logic.
    • Redux-actions -assertions Are used to test the assertions library of Redux Actions
    npm install --save redux-thunk
    
    npm install --save redux-saga
    
    npm install --save-dev fetch-mock redux-mock-store redux-actions-assertions
    
    npm install -g node-fetch
    Copy the code
  • Test synchronous action

    // actions/todoActions.js
    export const addTodo = text => ({ type: 'ADD_TODO', text });
    
    export const delTodo = text => ({ type: 'DEL_TODO', text });
    Copy the code
    // __tests__/actions/todoActions-test.js
    import * as actions from '.. /.. /actions/todoActions';
    
    describe('actions', () => {
      it('addTodo', () => {
        const text = 'hello redux';
        const expectedAction = {
          type: 'ADD_TODO',
          text,
        };
        expect(actions.addTodo(text)).toEqual(expectedAction);
      });
    
      it('delTodo', () => {
        const text = 'hello jest';
        const expectedAction = {
          type: 'DEL_TODO',
          text,
        };
        expect(actions.delTodo(text)).toEqual(expectedAction);
      });
    });
    Copy the code
  • Test asynchronous action based on redux-thunk

    // actions/fetchActions.js
    export const fetchTodosRequest = () => ({ type: 'FETCH_TODOS_REQUEST' });
    
    export const fetchTodosSuccess = data => ({
      type: 'FETCH_TODOS_SUCCESS',
      data,
    });
    
    export const fetchTodosFailure = data => ({
      type: 'FETCH_TODOS_FAILURE',
      data,
    });
    
    export function fetchTodos() {
      return dispatch => {
        dispatch(fetchTodosRequest());
        return fetch('http://example.com/todos')
          .then(res => res.json())
          .then(body => dispatch(fetchTodosSuccess(body)))
          .catch(ex => dispatch(fetchTodosFailure(ex)));
      };
    }
    Copy the code
    // __tests__/actions/fetchActions-test.js
    import configureMockStore from 'redux-mock-store';
    import thunk from 'redux-thunk';
    import fetchMock from 'fetch-mock';
    import * as actions from '.. /.. /actions/fetchActions';
    
    const middlewares = [thunk];
    const mockStore = configureMockStore(middlewares);
    
    describe('fetchActions', () => {
      afterEach(() => {
        fetchMock.restore();
      });
    
      it('Create FETCH_TODOS_SUCCESS after getting toDOS', async () => {
        fetchMock.getOnce('/todos', {
          body: { todos: ['do something'] },
          headers: { 'content-type': 'application/json'}}); FETCH_TODOS_REQUEST -> FETCH_TODOS_SUCCESS const expectedActions = [{type: 'FETCH_TODOS_REQUEST' },
          { type: 'FETCH_TODOS_SUCCESS', data: { todos: ['do something']]}}, const store = mockStore({ todos: [] }); // Process of optimizing asynchronous operations with async/await await store.dispatch(actions.fetchtodos ()); // Assert whether actios executes expect(store.getActions()).toequal (expectedActions); }); it('Create FETCH_TODOS_FAILURE after getting todos', async () => {
        fetchMock.getOnce('/todos', {
          throws: new TypeError('Failed to fetch')}); const expectedActions = [ {type: 'FETCH_TODOS_REQUEST' },
          { type: 'FETCH_TODOS_FAILURE', data: new TypeError('Failed to fetch')},]; const store = mockStore({ todos: [] }); await store.dispatch(actions.fetchTodos()); expect(store.getActions()).toEqual(expectedActions); }); });Copy the code
  • Test Sagas

    There are two main ways to test Sagas: test the Saga Generator function step by step, or execute the entire saga and assert Side Effects.

    • Test pure functions in Sagas Generator Function
    // sagas/uiSagas.js
    import { put, take } from 'redux-saga/effects';
    
    export const CHOOSE_COLOR = 'CHOOSE_COLOR';
    export const CHANGE_UI = 'CHANGE_UI';
    
    export const chooseColor = color => ({
      type: CHOOSE_COLOR,
      payload: {
        color,
      },
    });
    
    export const changeUI = color => ({
      type: CHANGE_UI,
      payload: {
        color,
      },
    });
    
    export function* changeColorSaga() {
      const action = yield take(CHOOSE_COLOR);
      yield put(changeUI(action.payload.color));
    }
    Copy the code
    // __tests__/sagas/uiSagas-test.js
    import { put, take } from 'redux-saga/effects';
    import {
      changeColorSaga, CHOOSE_COLOR, chooseColor, changeUI,
    } from '.. /.. /sagas/uiSagas';
    
    describe('uiSagas', () => {
      it('changeColorSaga', () => {
        const gen = changeColorSaga();
    
        expect(gen.next().value).toEqual(take(CHOOSE_COLOR));
    
        const color = 'red';
        expect(gen.next(chooseColor(color)).value).toEqual(put(changeUI(color)));
      });
    });
    Copy the code
    • Testing Side effects in Sagas Generator Function
    // sagas/fetchSagas.js
    import { put, call } from 'redux-saga/effects';
    
    export const fetchDatasSuccess = data => ({
      type: 'FETCH_DATAS_SUCCESS',
      data,
    });
    
    export const fetchDatasFailure = data => ({
      type: 'FETCH_DATAS_FAILURE',
      data,
    });
    
    exportconst myFetch = (... parmas) => fetch(... parmas).then(res => res.json());export function* fetchDatas() {
      try {
        const result = yield call(myFetch, '/datas'); yield put(fetchDatasSuccess(result)); } catch (error) { yield put(fetchDatasFailure(error)); }}Copy the code
    // __tests__/sagas/fetchSagas-test.js
    import { runSaga } from 'redux-saga';
    import { put, call } from 'redux-saga/effects';
    import fetchMock from 'fetch-mock';
    
    import {
      fetchDatas, fetchDatasSuccess, fetchDatasFailure, myFetch,
    } from '.. /.. /sagas/fetchSagas';
    
    describe('fetchSagas', () => { afterEach(() => { fetchMock.restore(); }); // Step generatorfunctionAnd assert side effects it('fetchDatas success', async () => {
        const body = { text: 'success' };
        fetchMock.get('/datas', {
          body,
          headers: { 'content-type': 'application/json'}}); const gen = fetchDatas(); // Next ().value expects (gen.next().value).toequal (call(myFetch,'/datas'));
    
        const result = await fetch('/datas').then(res => res.json()); expect(result).toEqual(body); // Expect (Gen. next(result).value).toequal (put(fetchDatasSuccess(body))); }); it('fetchDatas fail', () => {
        const gen = fetchDatas();
    
        expect(gen.next().value).toEqual(call(myFetch, '/datas')); Const throws = new TypeError(const throws = new TypeError)'Failed to fetch'); expect(gen.throw(throws).value).toEqual(put(fetchDatasFailure(throws))); }); // Execute the entire saga and assert Side Effects. (Recommended solution)'runSage success', async () => {
        const body = { text: 'success' };
        fetchMock.get('/datas', {
          body,
          headers: { 'content-type': 'application/json'}}); const dispatched = []; await runSaga({ dispatch: action => dispatched.push(action), }, fetchDatas).done; expect(dispatched).toEqual([fetchDatasSuccess(body)]); }); it('runSage fail', async () => {
        const throws = new TypeError('Failed to fetch');
        fetchMock.get('/datas', {
          throws,
        });
    
        const dispatched = [];
        await runSaga({
          dispatch: action => dispatched.push(action),
        }, fetchDatas).done;
    
        expect(dispatched).toEqual([fetchDatasFailure(throws)]);
      });
    });
    Copy the code
  • Test reducers

    // reducers/todos.js
    export default function todos(state = [], action) {
      switch (action.type) {
        case 'ADD_TODO':
          return [
            {
              text: action.text,
            },
            ...state,
          ];
    
        default:
          returnstate; }}Copy the code
    // __tests__/reducers/todos-test.js
    import todos from '.. /.. /reducers/todos';
    
    describe('reducers', () => {
      it('should return the initial state', () => {
        expect(todos(undefined, {})).toEqual([]);
      });
    
      it('todos initial', () => {
        expect(todos([{ text: '1' }], {})).toEqual([{ text: '1' }]);
      });
    
      it('should handle ADD_TODO', () => {
        expect(todos([], { type: 'ADD_TODO', text: 'text' })).toEqual([
          {
            text: 'text',}]); }); });Copy the code

The resources

  • jestjs.io/
  • airbnb.io/enzyme/
  • Redux.js.org/recipes/wri…
  • Juejin. Cn/post / 684490…