Unit test React components

Posted by dnzone on Mon, 31 Jan 2022 05:22:00 +0100

A characteristic of front-end development is that it will involve more user interfaces. When the development scale reaches a certain degree, it is almost doomed that its complexity will increase exponentially.

Whether in the initial construction of the code or in the subsequent unavoidable reconstruction and bug correction process, it is often difficult to sort out the logic and master the global correlation.

As a basic means of "outline and escort", unit test provides "fence and scaffold" for development, which can effectively improve these problems.

As a classic means of development and refactoring, unit testing is widely recognized and adopted in the field of software development; The front-end field has also gradually accumulated rich testing frameworks and best practices.

This article will be described in the following order:

  • 1. I ntroduction to unit testing
  • II. Tools used in react unit testing
  • 3. Test driven reconfiguration of React components
  • 4. Common cases of react unit test

1. I ntroduction to unit testing

unit testing refers to checking and verifying the smallest testable unit in software.

In short, a unit is the smallest function module under test that is artificially specified. Unit test is the lowest level test activity to be carried out in the process of software development. The independent unit of software will be tested in isolation from other parts of the program.

Test framework

The function of test framework is to provide some convenient syntax to describe test cases and group test cases.

Assertions

Assertion is the core part of the unit test framework. Failure of assertion will lead to test failure or report error information.

Some examples of common assertions are as follows:

  • Equality assertions

    • expect(sth).toEqual(value)
    • expect(sth).not.toEqual(value)
  • Comparative assertions

    • expect(sth).toBeGreaterThan(number)
    • expect(sth).toBeLessThanOrEqual(number)
  • Type assertions

    • expect(sth).toBeInstanceOf(Class)
  • Conditional test

    • expect(sth).toBeTruthy()
    • expect(sth).toBeFalsy()
    • expect(sth).toBeDefined()

Assertion Library

The assertion library mainly provides the semantic method of the above assertion, which is used to make various judgments on the values involved in the test. These semantic methods will return the test results, either successful or failed. A common assertion library is should js, Chai. JS et al.

test case

A set of test inputs, execution conditions, and expected results prepared for a specific goal to test a program path or verify whether a specific requirement is met.

The general form is:

it('should ...', function() {
		...
		
	expect(sth).toEqual(sth);
});

test suite

A set of related tests is usually called a test suite

The general form is:

describe('test ...', function() {
	
	it('should ...', function() { ... });
	
	it('should ...', function() { ... });
	
	...
	
});

spy

As "spy" literally means, we use this "spy" to "monitor" the call of functions

By wrapping the monitored function, you can clearly know how many times the function has been called, what parameters are passed in, what results are returned, and even the exceptions thrown.

var spy = sinon.spy(MyComp.prototype, 'componentDidMount');
...
expect(spy.callCount).toEqual(1);

stub

Sometimes, stub s are used to embed or directly replace some code to achieve the purpose of isolation

A stub can simulate the unit test with minimal dependencies. For example, one method may rely on the execution of another method, which is transparent to us. A good practice is to isolate and replace it with a stub. This enables more accurate unit testing.

var myObj = {
	prop: function() {
		return 'foo';
	}
};

sinon.stub(myObj, 'prop').callsFake(function() {
    return 'bar';
});

myObj.prop(); // 'bar'

mock

mock generally refers to a test method that uses a virtual object to create a test for some objects that are not easy to construct or obtain during the test process

Broadly speaking, the above spy and stub, as well as some simulations of modules, ajax return values and timer s, are all called mock.

Code coverage

It is used to count the test cases on the code and generate corresponding reports. For example, "istanbul" is a common test coverage statistics tool

II. Tools used in react unit testing

Jest

Different from the "traditional" (in fact, it hasn't appeared for a few years) front-end testing frameworks such as jasmine / Mocha / Chai - Jest is simpler to use, and provides higher integration and richer functions.

Jest is a test framework produced by Facebook. Compared with other test frameworks, one of its major features is that it has built-in common test tools, such as its own assertion and test coverage tools, which can be used out of the box.

In addition, the test cases of Jest are executed in parallel, and only the tests corresponding to the changed files are executed, which improves the test speed.

Four basic words

The syntax for writing unit tests is usually very simple; For jest, the use case syntax is the same as jasmine because it uses Jasmine 2 to test internally.

In fact, just memorizing these four words first is enough for most test situations:

  • describe: define a test suite
  • it: define a test case
  • expect: judgment condition of assertion
  • toEqual: comparison result of assertions
describe('test ...', function() {
	it('should ...', function() {
		expect(sth).toEqual(sth);
		expect(sth.length).toEqual(1);
		expect(sth > oth).toEqual(true);
	});
});

to configure

Jest claims to be a "Zero configuration testing platform". Just configure test: jest in _ npm scripts to run npm test and automatically identify and test the use case files that comply with its rules (generally under the __ directory).

In actual use, the appropriate custom configuration will get a more suitable test scenario:

//jest.config.js

module.exports = {
	modulePaths: [
		"<rootDir>/src/"
	], 
	moduleNameMapper: {
		"\.(css|less)$": '<rootDir>/__test__/NullModule.js'
	},
	collectCoverage: true,
	coverageDirectory: "<rootDir>/src/",
	coveragePathIgnorePatterns: [
		"<rootDir>/__test__/"
	],
	coverageReporters: ["text"],
};

In this simple configuration file, we specify the "root directory" of the test, configure some formats of coverage (built-in istanbul), and point the original reference to the style file in the webpack to an empty module, thus skipping this harmless link to the test

//NullModule.js
module.exports = {};

It is also worth mentioning that due to jest config. JS is a normal JS file that will be called in the npm script, not XXX JSON or XXXrc, so nodejs can perform their own operations, such as introducing fs for preprocessing, reading and writing. It is very flexible and can be well compatible with various projects

babel-jest

Because it is oriented to the src directory to test its React code, and also uses the ES6 syntax, there needs to be one under the project babelrc file:

{
  "presets": ["env", "react"]
}

The above is the basic configuration. In fact, because webpack can compile es6 modules, it is generally set to {"modules": false} in babel. At this time, the configuration is:

//package.json

"scripts": {
    "test": "cross-env NODE_ENV=test jest",
},
//.babelrc

{
  "presets": [
    ["es2015", {"modules": false}],
    "stage-1",
    "react"
  ],
  "plugins": [
  	"transform-decorators-legacy",
    "react-hot-loader/babel"
  ],
  "env": {
    "test": {
      "presets": [
	    "es2015", "stage-1", "react"
	  ],
	  "plugins": [
	  	"transform-decorators-legacy",
	    "react-hot-loader/babel"
	  ]
    }
  }
}

Enzyme

Enzyme comes from Airbnb, which is active in the JavaScript open source community. It is an encapsulation of the official test tool library (react addons test utils).

The London pronunciation of this word is ['enza] ɪ m] The meaning of enzyme or enzyme. Airbnb didn't design an icon for it. I guess it means to use it to decompose React components.

It simulates the API of jQuery, which is very intuitive and easy to use and learn. It provides some different interfaces and several methods to reduce the test template code, facilitate the judgment, manipulation and traversal of the output of React Components, and reduce the coupling between the test code and the implementation code.

In general, we use the mount or shallow method in Enzyme to transform the target component into a ReactWrapper object and invoke various methods in the test:

import Enzyme,{ mount } from 'enzyme';

...

describe('test ...', function() {
	
	it('should ...', function() {
		wrapper = mount(
			<MyComp isDisabled={true} />
		);
		expect( wrapper.find('input').exists() ).toBeTruthy();
	});
});

sinon

The "I lead the horse" in the picture is not the roller shutter general Sha Wujing In fact, the story in the picture is the well-known "Trojan horse"; The Greeks besieged the Trojans for more than ten years and couldn't attack them for a long time. They made a plan and withdrew the camp, leaving only a huge Trojan horse (filled with soldiers) and the man who was stripped naked and beaten badly, that is, the protagonist sin here, who deceived the Trojans - we are familiar with the following plot.

Therefore, this named testing tool is also a collection of various camouflage penetration methods. It provides independent and rich spy, stub and mock methods for unit testing, and is compatible with various testing frameworks.

Although Jest itself has some means to realize spy, etc., sinon is more convenient to use.

3. Test driven reconfiguration of React components

The classic theory of "test driven development" (TDD - test driven development) will not be discussed here -- in short, it is easy to understand that adding test forward to development, writing use cases first and then gradually realizing them is TDD.

However, when we go back and add test cases to the existing code to continuously improve its test coverage, improve the original design, repair potential problems, and ensure that the original interface will not be affected. Although no one calls this TDD behavior "test driven refactoring", However, the concept of "refactoring" itself includes the meaning of escorting with testing, which is an essential meaning in the question.

For some components and common functions, perfect testing is also the best instruction manual.

Fail code pass Trilogy

In the test results, the successful examples will be shown in green and the failed parts will be shown in red, so unit testing is often called "Red/Green Testing" or "Red/Green Refactoring", which is also a general step in TDD:

  1. Add a test
  2. Run all tests to see if the new one fails; If successful, repeat step 1
  3. Write or rewrite the code according to the failure and error report; The only purpose of this step is to pass the test without worrying about the details
  4. Run the test again; If successful, skip to step 5, otherwise repeat step 3
  5. Refactor the code that has passed the test to make it more readable and easier to maintain without affecting the passing of the test
  6. Repeat step 1

Interpretation of test coverage

This is the coverage result of the built-in "istanbul" output of "jest".

The reason why it is called "Istanbul" is that Turkish carpets are world-famous, and carpets are used to "cover" 🤦‍♀️.

Columns 2 to 5 in the table correspond to four measurement dimensions respectively:

  • statement coverage: whether each statement has been executed
  • branch coverage: whether each if code block is executed
  • function coverage: whether each function is called
  • line coverage: whether each line is executed

The test results are divided into "green, yellow and red" according to the coverage. The test coverage of corresponding modules should be improved as much as possible according to the specific situation.

Optimize dependencies to make React components testable

It will make unit testing easier to write componentized React reasonably and take sufficiently independent and functional components as test units;

On the contrary, the testing process makes it easier for us to clarify the relationship and reconstruct or decompose the original components into a more reasonable structure. The separated sub components are often easier to be written as stateless components, which makes the performance and concerns more optimized.

Specify PropTypes explicitly

For some components that are not clearly defined before, you can uniformly introduce {prop types to clarify the props that can be received by the component; On the one hand, errors can be found at any time during the development / compilation process. On the other hand, a clear list can be formed when other members of the team reference components.

4. Common cases of react unit test

Preprocessing or postprocessing of use cases

You can use beforeEach and afterEach to do some unified preset and aftercare work, which will be called automatically before and after each use case:

describe('test components/Comp', function() {

	let wrapper;
	let spy;

	beforeEach(function() {
		jest.useFakeTimers();
		
		spy = sinon.spy(Comp.prototype, 'componentDidMount');
	});
	afterEach(function() {
		jest.useRealTimers();
		
		wrapper && wrapper.unmount();
		
		didMountSpy.restore();
		didMountSpy = null;
	});
	
	it('The basic structure should be displayed correctly', function() {
		wrapper = mount(
			<Comp ... />
		);

		expect(wrapper.find('a').text()).toEqual('HELLO!');
	});
	
	...
	
});

Call the "private" method of the component

For some components, if you want to call some of their internal methods in the test phase and do not want to change the original component too much, you can use instance() to obtain the component class instance:

it('The component class instance should be obtained correctly', function() {
	var wrapper = mount(
		<MultiSelect
			name="HELLOKITTY"
			placeholder="select sth..." />
	);

	var wi = wrapper.instance();
	
	expect( wi.props.name ).toEqual( "HELLOKITTY" );
	expect( wi.state.open ).toEqual( false );
});

Test of asynchronous operation

As a UI component, some operations in the React component need to be delayed, such as high-frequency trigger actions such as onscroll or oninput, and function anti shake or throttling need to be done, such as the debounce of lodash.

The so-called asynchronous operation generally refers to this kind of operation without considering the integration test integrated with ajax. Only setTimeout is not enough. It needs to be used with the {done} function:

//In component

const Comp = (props)=>(
	<input type="text" id="searchIpt" onChange={ debounce(props.onSearch, 500) } />
);
//In unit test

it('The callback should be triggered on input', function(done) {
	var spy = jest.fn();

	var wrapper = mount(
		<Comp onChange={ spy } />
	);
	
	wrapper.find('#searchIpt').simulate('change');
	
	setTimeout(()=>{
		expect( spy ).toHaveBeenCalledTimes( 1 );
		done();
	}, 550);
});

Some global and singleton simulations

Some modules may be coupled with windows XXX is the reference of such global objects, and completely instantiating this object may involve many other problems, which is difficult to carry out; At this time, you can see the trick and trick, and only simulate a minimized global object to ensure the test:

//fakeAppFacade.js

var facade = {
	router: {
		current: function() {
			return {name:null, params:null};
		}
	},	appData: {
		symbol: "&yen;"
	}
};

window._appFacade = facade;
module.exports = facade;
//In the test suite

import fakeFak from '../fakeAppFacade';

In addition, for objects such as LocalStroage, there is no native support in the test side environment. You can also simply simulate:

//fakeStorage.js

var _util = {};
var fakeStorage = {
	"set": function(k, v) {
		_util['_fakeSave_'+k] = v;
	},
	"get": function(k) {
		return _util['_fakeSave_'+k] || null;
	},
	"remove": function(k) {
		delete _util['_fakeSave_'+k];
	},
	"has": function(k) {
		return _util.hasOwnProperty('_fakeSave_'+k);
	}
};
module.exports = fakeStorage;

Tricky react bootstrap / Modal

In a project, the "react bootstrap" interface library is used. When testing a component, because it contains its "Modal" pop-up window, and the pop-up window component is rendered into "document" by default, it is difficult to obtain it with the ordinary "find" method

The solution is to simulate an ordinary component rendered to the original location of the container component:

//FakeReactBootstrapModal.js

import React, {Component} from 'react';

class FakeReactBootstrapModal extends Component {
	constructor(props) {
		super(props);
	}
	render() { //Native react bootstrap / Modal cannot be tested by enzyme
		const {
			show,
			bgSize,
			dialogClassName,
			children
		} = this.props;
		return show
			? <div className={
				`fakeModal ${bgSize} ${dialogClassName}`
			}>{children}</div>
			: null;
	}
}

export default FakeReactBootstrapModal;

At the same time, judgment logic is added during component rendering, so that it can support custom classes instead of Modal classes:

//ModalComp.js

import { Modal } from 'react-bootstrap';

...

render() {
	const MyModal = this._modalClass || Modal;
			
	return (<MyModal 
		bsSize={props.mode>1 ? "large" : "middle"}		dialogClassName="custom-modal">
		
		...
		
		</MyModal>;
}

In the test suite, a test specific subclass is implemented:

//myModal.spec.js

import ModalComp from 'components/ModalComp';

class TestModalComp extends ModalComp {
	constructor(props) {
		super(props);
		this._modalClass = FakeReactBootstrapModal;
	}
}

In this way, the test can proceed smoothly, skip the unimportant UI effect, and all kinds of logic can be covered

Simulate fetch request

In the process of unit testing, it is inevitable to encounter some situations that require remote data request, such as component obtaining initialization data, submitting change data, etc.

It should be noted that the purpose of this test is to examine the performance of the component itself, rather than focusing on the integration test of actual remote data, so we can simply simulate some requested scenarios without real requests.

Sin has some methods to simulate XMLHttpRequest requests, and jest also has some third-party libraries to solve fetch tests;

In our project, according to the actual usage, we implement a class to simulate the response of the request:

//FakeFetch.js

import { noop } from 'lodash';

const fakeFetch = (jsonResult, isSuccess=true, callback=noop)=>{

	const blob = new Blob(
		[JSON.stringify(jsonResult)],
		{type : 'application/json'}
	);

	return (...args)=>{
		console.log('FAKE FETCH', args);

		callback.call(null, args);

		return isSuccess
			? Promise.resolve(
				new Response(
					blob,
					{status:200, statusText:"OK"}
				)
			)
			: Promise.reject(
				new Response(
					blob,
					{status:400, statusText:"Bad Request"}
				)
			)

	}
};
export default fakeFetch;
//Comp.spec.js

import fakeFetch from '../FakeFetch';

const _fc = window.fetch; //Cache "real" fetch

describe('test components/Comp', function() {

	let wrapper;
	
	afterEach(function() {
		wrapper && wrapper.unmount();
		window.fetch = _fc; //recovery
	});
	
	it("Should respond to remote requests onRemoteData", (done)=>{

		window.fetch = fakeFetch({
			brand: "GoBelieve",
			tree: {
		      node: 'headquarters',
		      children: null
		    }
		});

		let spy = jest.fn();

		wrapper = mount(
			<Comp onRemoteData={ spy } />
		);

		jest.useRealTimers();

		_clickTrigger(); //The request should be initiated at this time

		setTimeout(()=>{
			expect(wrapper.html()).toMatch(/headquarters/);
			expect(spy).toHaveBeenCalledTimes(1);
			done();
		}, 500);

	});
	
});

5. Summary

As a classical means of development and reconstruction, unit testing is widely recognized and adopted in the field of software development; The front-end field has also gradually accumulated a wealth of testing frameworks and methods.

Unit testing can provide basic guarantee for our development and maintenance, and enable us to complete the construction and reconstruction of the code with clear ideas and a bottom in mind;

It should be noted that there is no cure all in the world, and unit testing is by no means a panacea. Only by taking a cautious, serious and responsible attitude can we fundamentally ensure the progress of our work.

Finally: [may help you] then share some of my self-study materials below, hoping to help you.

This material is organized around [software testing] as a whole. The main content includes: exclusive video of Python automatic testing, detailed information of Python automation, a full set of interview questions and other knowledge content. For friends of software testing, it should be the most comprehensive and complete war preparation warehouse. This warehouse has also accompanied me through many rough roads. I hope it can also help you.

Add group: 716055499, you can get it directly. There are test cows in the group to share experience.

If my blog is helpful to you and you like my blog content, please click "like", "comment" and "collect" for three times!

Topics: React unit testing software testing Testing