“Writing software is the hardest thing a human being can do” ———— Douglas Crockford As programmers, our job is not just to write new code, a lot of time we are maintaining and debugging other people’s code. Testable code is easier to test, which means it’s easier to maintain; Maintained means it’s easier to understand — and easier to understand, which makes testing easier. As the front-end business becomes more and more complex, unit testing becomes more and more important in front-end engineering. A test consisting of testable code can help us understand the impact of seemingly minor changes and ensure that our changes do not affect other features, so that we can fix and change the code better. This article will introduce some relevant basic knowledge from the perspective of unit testing, and then explore some attempts of Jest and Enzyme in the front-end application developed based on React.

What are unit tests

Unit testing ensures the robustness of code by testing and validating the smallest testable unit, usually a single function or group. Unit testing is a developer’s first line of defense. Unit tests not only force developers to understand our code, they also help us document and debug it. Good unit test cases can even serve as development documentation for developers to read.

What is not a unit test

Tests that need to access a database that need network communication are not unit tests that need to call a file system. Tests that need to do specific configuration to the environment (such as editing configuration files) to run are not unit tests — the art of modifying code

The basic concept

Test Suite/Case Aggregation

The most important part of a unit testing framework is aggregating tests into test suites and test cases. Test suites and test cases are scattered across many files, and each test file usually contains tests for a single module. The best way to do this is to consolidate all the tests for a single module into a single test suite. The test suite consists of multiple test cases, with each test module testing only a small portion of the module’s functionality. You can clean up before and after tests by using the setup and teardown functions at the test suite and test case levels.

Assertions (an Assertion)

At the heart of unit testing is the assertion that we use to determine if the code is doing what it’s trying to do. Assert keywords such as assert should expect are commonly used. Assert is the simplest, while Expect is closer to the normal reading order. For assertion keywords, look at CHAI. This assertion library is powerful and provides support for a variety of assertion keywords.

Dependency (Dependencies)

Unit tests should be loaded on the smallest unit required for testing, and any additional code may affect the test or the code being tested. To avoid loading external dependencies, we can use mocks, stubs, and test doubles. They all try to isolate the code being tested from other code as much as possible.

Test double

The test surrogate described uses stubs or mocks to test dependent objects. At the same time, proxies can be represented as stubs or mocks to ensure that external methods and apis are called, log the number of times they are called, capture the call parameters, and return a response. A test surrogate who can record method calls and capture information about methods being called is called a spy.

1. Mould (mock)

Mock objects are used to verify that functions can call external apis correctly. Unit tests verify that the function being tested passes the correct arguments to external objects by introducing mock objects.

2. Pile (stubs)

Stub objects are used to return the encapsulated value to the function under test. A stub object does not care how the external object method is called; it simply returns the selected encapsulated object.

3. The spy (spy)

A SPY is usually attached to a real object, intercepting method calls (sometimes even intercepting method calls with specific parameters) to return the wrapped response content or track how many times a method was called. Methods that are not intercepted are processed on the real object as normal.

Code Coverage

Code coverage is a measure of test integrity and is usually divided into two parts: line coverage and function coverage. In theory, the more lines of code you “cover”, the more complete your test will be. But from my personal point of view:

The primary purpose of unit testing is not to be able to write all-pass test code with great coverage, but rather to experiment with the possibilities of functional logic from the perspective of the user (caller), thereby enhancing the quality of the code.

What is a Jest

Jest a unit test framework is Facebook: Jest applies to React only not just, also provides for the Node/presents/Vue support/Typescript.

Jest features:

  1. Ease of use: Based onJasmineThat incorporatesexpectAssertions and multiple matchers
  2. Adaptability: Modular, easy to expand and configure
  3. Snapshot testing: Deep comparisons can be made automatically by taking snapshots of components or data
  4. Asynchronous testing: Supportedcallback promise async/awaitThe test of
  5. Mock system: Provides a powerful set ofmockSystem that supports automatic or manual mocks
  6. Static analysis result generation: integrate Istanbul to generate test coverage reports

The basic concept

Matchers

In Jest, expect assertions combined with Matchers can help us test code in a variety of ways. See Expect for more, and here are some basic examples:

describe('common use of matchers', () => {
  it('two plus two equal four', () => {
    expect(2 + 2).toBe(4);
  });
  it('check value of an object', () = > {const obj = { id: 1.name: 'test' };
    obj['name'] = 'nameChanged';
    expect(obj).toEqual({ id: 1.name: 'nameChanged' });
  });
  it('case of truthiness', () = > {const n = null;
    expect(n).toBeNull();
    expect(n).toBeDefined();
    expect(n).not.toBeUndefined();
    expect(n).not.toBeTruthy();
    expect(n).toBeFalsy();
  });
  it('case of numbers', () = > {const value = 2 + 1;
    expect(value).toBeGreaterThanOrEqual(3);
    expect(value).toBeGreaterThan(2);
    expect(value).toBeLessThan(4);
    expect(value).toBeLessThanOrEqual(3);
  });
  it('case of float numbers', () = > {const value = 0.1 + 0.2;
    expect(value).toBeCloseTo(0.3);
  });
  it('case of array and iterables', () = > {const fruits = ['apple'.'banana'.'cherry'.'pear'.'orange'];
    expect(fruits).toContain('banana');
    expect(new Set(fruits)).toContain('pear');
  });
  it('case of exceptions', () = > {const loginWithoutToken = (a)= > {
      throw new Error('You are not authorized');
    };
    expect(loginWithoutToken).toThrow();
    expect(loginWithoutToken).toThrow('You are not authorized');
  });
});
Copy the code

Setup and Teardown

In unit test writing, we often need to do some preparation work before the test starts and tidy up after the test runs. Jest provides methods to help us do this. If we want a one-time setting, we can use beforeAll and afterAll to handle:

beforeAll((a)= > {
  // Preprocessing operations
});

afterAll((a)= > {
  // Clean up the work
});
test('has foo', () => {
  expect(testObject.foo).toBeTruthy();
})
Copy the code

If we want to set up and clean up before and afterEach test, we can use beforeEach and afterEach:

beforeEach((a)= > {
  // Preprocessing before each test
});

afterEach((a)= > {
  // Clean up after each test
});
test('has foo', () => {
  expect(testObject.foo).toBeTruthy();
})
Copy the code

Testing asynchronous code

Jest also provides good support for testing asynchronous code, such as: 1. To test callback, suppose we have a callback for fetchData(callback) :

const helloCallback = (name: string, callback: (name: string) = > void) => {
  setTimeout((a)= > {
    callback(`Hello ${name}`);
  }, 1000);
};

test('should get "Hello Jest"', done => {
  helloCallback('Jest', result => {
    expect(result).toBe('Hello Jest');
    done();
  });
});
Copy the code

2. Test promise and async\await:

const helloPromise = (name: string) = > {
  return new Promise(resolve= > {
    setTimeout((a)= > {
      resolve(`Hello ${name}`);
    }, 1000);
  });
};

test('should get "Hello World"', () => {
  expect.assertions(1);
  return helloPromise('Jest').then(data= > {
    expect(data).toBe('Hello Jest');
  });
});

test('should get "Hello World"'.async () => {
  expect.assertions(1);
  const data = await helloPromise('Jest');
  expect(data).toBe('Hello Jest');
});
Copy the code

mock functions

Jest provides a convenient method to simulate mocking functions. Here is a mocking module example. Please refer to the official website for more examples.

// users.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp= >resp.data); }}export default Users;

// users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');
test('should fetch users', () = > {const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);
  return Users.all().then(data= > expect(data).toEqual(users));
});
Copy the code

What is the Enzyme

Enzyme is a JavaScript test tool for React developed by Airbnb, which encapsulates the official test tool library (React-Addons-test-utils). Cheerio uses the Cheerio library to parse the virtual DOM and provides a jquery-like API to manipulate the virtual DOM, which makes it easy to judge, manipulate, and iterate the React Components output in unit tests.

Three rendering methods

shallow([options]) => ShallowWrapper

Shallow method is the encapsulation of official shallow Rendering. Shallow rendering renders only the first layer of the component’s DOM structure, and its children are not rendered, ensuring efficient rendering and fast unit testing.

import { shallow } from 'enzyme';

describe('enzyme shallow rendering', () => {
  it('todoList has three todos', () = > {const todoList = shallow(<App />);
    expect(todoList.find('.todo')).toHaveLength(3);
  });
});
Copy the code

mount(node[, options]) => ReactWrapper

The Mount method renders the React Components as real DOM nodes, suitable for scenarios where you need to test Components that use the DOM API. If the tests are run in the same DOM environment, they may affect each other. In this case, use the unmount method provided by the Enzyme to clean up the tests.

import { mount } from 'enzyme';

describe('enzyme full rendering', () => {
  it('todoList has none todos done', () = > {const todoList = mount(<TodoList />);
    expect(todoList.find('.todo-done')).toHaveLength(0);
  });
});
Copy the code

render() => CheerioWrapper

The Render method returns a static HTML string rendered by the React Components wrapped in CherrioWrapper. The CherrioWrapper will help us analyze the HTML structure of the final code.

import { render } from 'enzyme';

describe('enzyme static rendering', () => {
  it('no done todo items', () = > {const todoList = render(<TodoList />);
    expect(todoList.find('.todo-done')).toHaveLength(0);
    expect(todoList.html()).toContain(<div className="todo" />);
  });
});
Copy the code

Selector and simulation events

Regardless of the rendering method, the wrapper returned has a find method that takes a selector parameter and returns a Wrapper object of the same type. Similarly, methods like at Last First can select location-specific child components, and a SIMULATE method can simulate an event on a component. Selectors in the Enzyme are like CSS Selectors, and if you need to support complex CSS Selectors, you need to introduce the findDOMNode method from the React-DOM.

// class selector
wrapper.find('.bar')
// tag selector
wrapper.find('div')
// id selector
wrapper.find('#bar')
// component display name
wrapper.find('Foo')
// property selector
const wrapper = shallow(<Foo />)
wrapper.find({ prop: 'value] }))
Copy the code

Testing component status

Enzyme provides methods like setState and setProps that can be used to simulate changes in state and props. And similar things like setContext and so on. Note that the setState method can only be used on root Instance.

//  set state
interface IState {
  name: string;
}
class Foo extends React.Component<any, IState> {
  state = { name: 'foo' };
  render() {
    const { name } = this.state;
    return <div className={name}>{name}</div>;
  }
}
const wrapper = shallow(<Foo />);
expect(wrapper.find('.foo')).toHaveLength(1);
expect(wrapper.find('.bar')).toHaveLength(0);
wrapper.setState({ name: 'bar' });
expect(wrapper.find('.foo')).toHaveLength(0);
expect(wrapper.find('.bar')).toHaveLength(1);
Copy the code
// set props
interface IProps {
  name: string;
}
function Foo({ name }: IProps) {
  return <div className={name} />;
}
const wrapper = shallow(<Foo name="foo"/ >); expect(wrapper.find('.foo')).toHaveLength(1);
expect(wrapper.find('.bar')).toHaveLength(0);
wrapper.setProps({ name: 'bar' });
expect(wrapper.find('.foo')).toHaveLength(0);
expect(wrapper.find('.bar')).toHaveLength(1);
Copy the code

Instance to explain

Configuration and initial testing

Since our projects are developed based on UMI and the UMI framework already integrates WITH Jest, examples are created based on Jest. Test React App with Jest and Enzyme. Here, I’ll show you how to configure the other dependencies and do some preliminary test writing. SRC \pages\__tests__\index.test. TSX: SRC \pages\__tests__\index.test. TSX: SRC \pages\__tests__\index.test. TSX: SRC \pages\__tests__\index.

describe('Page: index'.(a)= > {
  it('Render correctly'.(a)= > {
    const wrapper: ReactTestRenderer = renderer.create(<Index />);
    expect(wrapper.root.children.length).toBe(1);
    const outerLayer = wrapper.root.children[0] as ReactTestInstance;
    expect(outerLayer.type).toBe('div');
    expect(outerLayer.children.length).toBe(2);
  });
})
Copy the code

Add the dependent

Then we need to add the Enzyme. In React 16.x, we also need the Enzyme-Adapter-16. In addition, we need to add typescript type-definition dependencies:

yarn add -D enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16
Copy the code

Then add the following code to the previously opened file:

import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Configure the adapter for the enzyme
configure({ adapter: new Adapter() });
Copy the code

Rewrite the test with Enzyme

describe('Page: index'.(a)= > {
  it('Render correctly'.(a)= > {
    const wrapper = mount(<Index />);
    expect(wrapper.children()).toHaveLength(1);
    const outerLayer = wrapper.childAt(0);
    expect(outerLayer.type()).toBe('div');
    expect(outerLayer.children()).toHaveLength(2);
  });
});
Copy the code

Run UMI test and you will see the following information in the console:

Test the React Hooks

Then open index.tsx and introduce useState, and add the following code at the top of function:

const [myState, setMyState] = useState('Welcome to Umi');
const changeState = (a)= > setMyState('Welcome to Jest and Enzyme');
Copy the code

Then add the following code:

<a href="https://umijs.org/guide/getting-started.html">
  Getting Started
</a>
Copy the code

Replace with:

<div id="intro">{myState}</div>
<button onClick={changeState}>Change</button
Copy the code

Then add the following test code:

it('case of use state'.(a)= > {
  const wrapper = shallow(<Index />);
  expect(wrapper.find('#intro').text()).toBe('Welcome to Umi');
  wrapper.find('button').simulate('click');
  expect(wrapper.find('#intro').text()).toBe('Welcome to Jest and Enzyme');
})
Copy the code

Run UMI test and you can see that our tests have taken effect.

Adding snapshot Tests

As mentioned earlier, Jest supports snapshot testing. Now add a snapshot for Index. First we add the following dependencies:

yarn add -D enzyme-to-json @types/enzyme-to-json
Copy the code

Then add the following code to the test case:

  it('matches snapshot'.(a)= > {
    const wrapper = shallow(<Index />);
    expect(toJson(wrapper)).toMatchSnapshot();
  });
Copy the code

After running umi test again, we can see that snapshot has been generated:

Todo List sample

Let’s write a relatively complete example, todo List, that integrates Redux and does the following:

  1. Enter the todo content and click the Create button to submit
  2. Shows the list of toDos created
  3. Click Todo to delete the entry

For details, please refer to SRC \pages\todoDemo\index.tsx. The test code is as follows:

describe('<TodoList />'.(a)= > {
  it('matches snapshot'.(a)= > {
    const todos: Array<todo> = [];
    const wrapper = shallow(<TodoList todos={todos} />);
    expect(toJson(wrapper)).toMatchSnapshot();
  });
  it('calls setState after input change'.(a)= > {
    const wrapper = shallow(<TodoList todos={[]} />);
    wrapper.find('input').simulate('change', { target: { value: 'Add Todo'}}); expect(wrapper.state('input')).toEqual('Add Todo');
  });
  it('calls addTodo with submit button click'.(a)= > {
    const addTodo = jest.fn();
    const todos: Array<todo> = [];
    const wrapper = shallow(<TodoList todos={todos} addTodo={addTodo} />);
    wrapper.find('input').simulate('change', { target: { value: 'Add Todo'}}); wrapper.find('.todo-add').simulate('click');
    expect(addTodo).toHaveBeenCalledWith('Add Todo');
  });
  it('calls removeTodo with todo item click'.(a)= > {
    const removeTodo = jest.fn();
    const todos: Array<todo> = [{ text: 'Learn Jest' }, { text: 'Learn RxJS' }];
    const wrapper = shallow(<TodoList todos={todos} removeTodo={removeTodo} />);
    wrapper
      .find('li')
      .at(0)
      .simulate('click');
    expect(removeTodo).toHaveBeenCalledWith(0);
  });
})
Copy the code

Life cycle Example

Based on Jest and Enzyme, we can also conveniently listen for life cycle changes:

import React from 'react';
import { shallow } from 'enzyme';

const orderCallback = jest.fn();

interface LifecycleState {
  currentLifeCycle: string;
}

class Lifecycle extends React.Component<any, LifecycleState> {
  static getDerivedStateFromProps() {
    orderCallback('getDerivedStateFromProps');
    return { currentLifeCycle: 'getDerivedStateFromProps' };
  }

  constructor(props: any) {
    super(props);
    this.state = { currentLifeCycle: 'constructor' };
    orderCallback('constructor');
  }

  componentDidMount() {
    orderCallback('componentDidMount');
    this.setState({
      currentLifeCycle: 'componentDidMount',
    });
  }

  componentDidUpdate() {
    orderCallback('componentDidUpdate');
  }

  render() {
    orderCallback('render');
    return <div>{this.state.currentLifeCycle}</div>;
  }
}

describe('React Lifecycle', () => {
  beforeEach(() => {
    orderCallback.mockReset();
  });

  it('renders in correct order', () => {
    const _ = shallow(<Lifecycle />);
    expect(orderCallback.mock.calls[0][0]).toBe('constructor');
    expect(orderCallback.mock.calls[1][0]).toBe('getDerivedStateFromProps');
    expect(orderCallback.mock.calls[2][0]).toBe('render');
    expect(orderCallback.mock.calls[3][0]).toBe('componentDidMount');
    expect(orderCallback.mock.calls[4][0]).toBe('getDerivedStateFromProps');
    expect(orderCallback.mock.calls[5][0]).toBe('render');
    expect(orderCallback.mock.calls[6][0]).toBe('componentDidUpdate');
    expect(orderCallback.mock.calls.length).toBe(7);
  });

it('detect lify cycle methods', () => {
    const _ = shallow(<Lifecycle />);
    expect(Lifecycle.getDerivedStateFromProps.call.length).toBe(1);
    expect(Lifecycle.prototype.render.call.length).toBe(1);
    expect(Lifecycle.prototype.componentDidMount.call.length).toBe(1);
    expect(Lifecycle.getDerivedStateFromProps.call.length).toBe(1);
    expect(Lifecycle.prototype.render.call.length).toBe(1);
    expect(Lifecycle.prototype.componentDidUpdate.call.length).toBe(1);
  });
});
Copy the code

Issues to be aware of in Antd

Antd source code has a very complete unit test support, interested in can go to study, here do not expand, only on the previous pit analysis:

  1. Form submission event: This is a test code from antD source code. I modified it to add a submission event:
class Demo extends React.Component<FormComponentProps> { reset = () => { const { form } = this.props; form.resetFields(); }; onSubmit = () => { const { form } = this.props; form.resetFields(); // Commit action}; render() { const { form: { getFieldDecorator }, } = this.props; return ( <Form onSubmit={this.onSubmit}> <Form.Item>{getFieldDecorator('input', { initialValue: '' })(<Input />)}</Form.Item> <Form.Item> {getFieldDecorator('textarea', { initialValue: '' })(<Input.TextArea />)} </Form.Item> <button type="button" onClick={this.reset}> reset </button> <button type="submit">submit</button> </Form> ); }}Copy the code

To test the reset form event, we just need to simulate the reset button click time:

  it('click to reset'.(a)= > {
    const wrapper = mount(<FormDemo />);
    wrapper.find('input').simulate('change', { target: { value: '111'}}); wrapper.find('textarea').simulate('change', { target: { value: '222'}}); expect(wrapper.find('input').prop('value')).toBe('111');
    expect(wrapper.find('textarea').prop('value')).toBe('222');
    wrapper.find('button[type="button"]').simulate('click');
    expect(wrapper.find('input').prop('value')).toBe(' ');
    expect(wrapper.find('textarea').prop('value')).toBe(' ');
  });
Copy the code

If you want to test the form’s submit event, you should simulate the form’s Submit event (unless the submit event is bound to a button element and the button type is “button”).

  it('click to submit'.(a)= > {
    const wrapper = mount(<FormDemo />);
    wrapper.find('input').simulate('change', { target: { value: '111'}}); wrapper.find('textarea').simulate('change', { target: { value: '222'}}); expect(wrapper.find('input').prop('value')).toBe('111');
    expect(wrapper.find('textarea').prop('value')).toBe('222');
    wrapper.find('form').simulate('submit');
    expect(wrapper.find('input').prop('value')).toBe(' ');
    expect(wrapper.find('textarea').prop('value')).toBe(' ');
  });
Copy the code
  1. aboutInput.Search:

Antd’s Input and input. TextArea can directly simulate onChange events, but onSearch in input. Search is not a DOM native event, so we need to test it like this:

describe('antd event test', () => {
  it('test search event', () => {
    const mockSearch = jest.fn();
    const wrapper = mount(
      <div>
        <Search onSearch={mockSearch} />
      </div>,
    );
    const onSearch = wrapper.find(Search).props().onSearch;
    if (onSearch !== undefined) {
      onSearch(searchText);
      expect(mockSearch).toBeCalledWith(searchText);
    } else {
      expect(mockSearch).not.toBeCalled();
    }
  });
});
Copy the code

Test coverage

Finally, we run umi test — Coverage to see the final coverage data. The untested codes are mapDispatchToProps and mapStateToProps

The resources

  1. Testable JavaScript Ensuring Reliable Code, Mark Trostler
  2. TypeScript-React-Starter
  3. The Art of Modifying code by Michael Feathers