Author: Jiang Shui
This paper mainly introduces some technical schemes of front-end unit testing.
There are many technical solutions for unit testing, and there are many synergies and overlaps between different tools, which make it difficult to match test solutions. Moreover, with the advent of ES6 and TypeScript, unit testing has added many additional steps, and complete configuration is often time consuming. I hope to master the respective functions of these tools to understand the complete technical solution of front-end testing. There are also many fields of front-end unit testing. This section mainly introduces how to conduct unit testing for front-end components, and finally summarizes some test methods for React components.
Universal test
A core part of unit testing is making assertions, such as the assert function in traditional languages, that if the current program has a state that meets its expectations, the program should run properly, and if it doesn’t, it should quit the application. So we can use Node’s built-in assert module directly to make assertions.
Let’s do the simplest example we can
function multiple(a, b) {
let result = 0;
for (let i = 0; i < b; ++i)
result += a;
return result;
}
Copy the code
const assert = require('assert');
assert.equal(multiple(1.2), 3));
Copy the code
Such examples can be used for basic scenarios or as a method of unit testing.
Nodejs’s assert module provides the following assertion methods, which can only be used in simple scenarios.
assert.deepEqual(actual, expected[, message])
assert.deepStrictEqual(actual, expected[, message])
assert.doesNotMatch(string, regexp[, message])
assert.doesNotReject(asyncFn[, error][, message])
assert.doesNotThrow(fn[, error][, message])
assert.equal(actual, expected[, message])
assert.fail([message])
assert.ifError(value)
assert.match(string, regexp[, message])
assert.notDeepEqual(actual, expected[, message])
assert.notDeepStrictEqual(actual, expected[, message])
assert.notEqual(actual, expected[, message])
assert.notStrictEqual(actual, expected[, message])
assert.ok(value[, message])
assert.rejects(asyncFn[, error][, message])
assert.strictEqual(actual, expected[, message])
assert.throws(fn[, error][, message])
Copy the code
The built-in assert is not intended for use by unit tests and provides poorly documented error messages. When executed, the above demo will produce the following report:
$ node index.js assert.js:84 throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: 2 == 3 at Object.<anonymous> (/home/quanwei/git/index.js:4:8) at Module._compile (internal/modules/cjs/loader.js:778:30) at Object.Module._extensions.. js (internal/modules/cjs/loader.js:789:10) at Module.load (internal/modules/cjs/loader.js:653:32) at tryModuleLoad (internal/modules/cjs/loader.js:593:12) at Function.Module._load (internal/modules/cjs/loader.js:585:3) at Function.Module.runMain (internal/modules/cjs/loader.js:831:12) at startup (internal/bootstrap/node.js:283:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)Copy the code
Because the built-in modules depend on the version of Node and cannot be updated freely, the flexibility of using the built-in package is sometimes not enough. Moreover, many of our assertion functions also need to be executed on the browser side, so we need to support both the browser and Node side assertion library. Also looking at the output above, you can see that this report is more like a bug report for the program than a unit test report. When we do unit testing, we often need the assertion library to provide good test reports, so that we can see at a glance which assertions are passed or not, so it is necessary to use a professional unit test assertion library.
chai
Chai is a popular assertion library that stands out from its peers. Chai provides two styles of assertion functions, TDD (Test-driven Development) and BDD (Behavior-Driven Development). The advantages and disadvantages of the two styles will not be discussed here, but BDD style will be used for demonstration in this paper.
TDD style CHAI
var assert = require('chai').assert
, foo = 'bar'
, beverages = { tea: [ 'chai'.'matcha'.'oolong']}; assert.typeOf(foo,'string'); // without optional message
assert.typeOf(foo, 'number'.'foo is a number'); // with optional message
assert.equal(foo, 'bar'.'foo equal `bar`');
assert.lengthOf(foo, 3.'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3.'beverages has 3 types of tea');
Copy the code
Chai adds an assertion argument to Node’s assert to improve the readability of test reports
$ node chai-assert.js
/home/quanwei/git/learn-tdd-bdd/node_modules/chai/lib/chai/assertion.js:141
throw new AssertionError(msg, {
^
AssertionError: foo is a number: expected 'bar'to be a number at Object.<anonymous> (/home/quanwei/git/learn-tdd-bdd/chai-assert.js:6:8) at Module._compile (internal/modules/cjs/loader.js:778:30) at Object.Module._extensions.. js (internal/modules/cjs/loader.js:789:10) at Module.load (internal/modules/cjs/loader.js:653:32) at tryModuleLoad (internal/modules/cjs/loader.js:593:12) at Function.Module._load (internal/modules/cjs/loader.js:585:3) at Function.Module.runMain (internal/modules/cjs/loader.js:831:12) at startup (internal/bootstrap/node.js:283:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)Copy the code
BDD style chai
The CHAI STYLE of BDD uses Expect functions as a semantic starting point, and it is the style that almost all BDD tool libraries follow today.
Chai’s Expect assertion style is as follows
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
Copy the code
The idea of BDD is that writing unit tests is like writing product requirements, regardless of internal logic, and that each use case reads like a document. For example, the following use case:
- Foo is a string ->
expect(foo).to.be.a('string')
- Foo string contains ‘bar’ ->
expect(foo).to.include('bar')
- Foo string does not contain ‘biz’ ->
expect(foo).to.not.include('biz')
You can see that test cases in this style are more readable.
Other assertion libraries include expect. Js should. Js Better-assert, unexpected.
Now that we have an assertion library, we also need to use a testing framework to better organize our assertions.
Mocha and Jasmine
Mocha is a classic Test Framework. The Test Framework provides a unit Test Framework, which can divide different sub-functions into multiple files and Test different sub-functions of a sub-module to generate a structured Test report. Mocha, for example, provides describe and IT describe use-case constructs, before, after, beforeEach, afterEach lifecycle functions, Describe. Only,describe. Skip, IT. Only, it. Skip are provided to execute specified parts of the test set.
const { expect } = require('chai');
const { multiple } = require('./index');
describe('Multiple'.() = > {
it ('should be a function'.() = > {
expect(multiple).to.be.a('function');
})
it ('expect 2 * 3 = 6'.() = > {
expect(multiple(2.3)).to.be.equal(6); })})Copy the code
The testing framework does not rely on the underlying assert library, even with the native Assert module. It’s a hassle to manually import CHAI for each file, so you can configure mocha’s global script and load the assertion library in the project root directory.mocharc.js so that each file can use expect functions directly.
// .mocharc.js
global.expect = require('chai').expect;
Copy the code
Using Mocha, we can output our unit tests as a good test report mocha *.test.js
The output is as follows when an error occurs
Because running in different environments requires different package formats, we need to do different package format conversion for different environments. In order to understand what needs to be done to run unit tests on different ends, we can first take a look at the common package formats.
Currently we have three main Module formats, namely AMD, CommonJS and ES Module.
AMD
AMD is an older specification popular in the RequireJS rollout, and is not currently supported by default in browsers or Nodes. The AMD standard defines define and require functions. Define defines a module and its dependencies, and require loads a module. For example,
<! doctype html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>Document</title>+
SRC = "https://requirejs.org/docs/release/2.3.6/minified/require.js" > < / script >
+
</head>
<body></body>
</html>
Copy the code
// index.js
define('moduleA'['https://some/of/cdn/path'].function() {
return { name: 'moduleA' };
});
define(function(require) {
const fs = require('fs');
return fs;
})
define('moduleB'.function() {
return { name: 'module B'}});require(['moduleA'.'moduleB'].function(moduleA, moduleB) {
console.log(module);
});
Copy the code
Using RequireJS as the AMD engine, you can see that the define function defines which modules are currently dependent on and asynchronously calls back to the current module after the module is loaded. This feature makes AMD particularly suitable for browser-side asynchronous loading.
We can use Webpack to package an AMD module to see the actual code
// entry.js
export default function sayHello() {
return 'hello amd';
}
Copy the code
// webpack.config.js
module.exports = {
mode: 'development'.devtool: false.entry: './entry.js'.output: {
libraryTarget: 'amd'}}Copy the code
Final code generation (simplifies irrelevant logic)
// dist/main.js
define(() = > ({
default: function sayHello() {
return 'hello amd'; }}));Copy the code
To use AMD in a browser /Node requires the global introduction of RequireJS. A typical problem for unit testing is to ask whether to use RequireJS when initializing karma, but it is rarely used nowadays.
CommonJS
The CJS specification is designed to define the package format of Node. CJS defines three keywords: require, Exports, Module, almost all Node packages and front-end related NPM packages can be converted to this format. CJS needs to be packaged using webpack or Browserify on the browser side before it can be executed.
ES Module
ES Module is a Module specification defined in ES 2015, which defines import and export, and is a commonly used format in our development.
The following table shows the range of support for each format, with parentheses indicating the need for external tool support.
Node | The browser | |
---|---|---|
AMD | Not supported (require.js, r.js) | Does not support (the require. Js) |
CommonJS | support | Does not support (webpack/browserify) |
ESModule | Does not support (Babel) | Does not support (webpack) |
Unit tests that run in different environments need to be packaged in different environments, so determine what environment you’re running in when building your test toolchain. If you’re running in Node, you just need to add a layer of Babel transformation, and if you’re running in a real browser, you need to add webPack processing steps.
So there are two ways to be able to use ES Module in Mocha of a Node environment
Node
The environment is naturally supportiveES Module
(node version >= 15)- use
babel
The code performs a transformation
The first method is skipped, and the second method uses the following configuration
npm install @babel/register @babel/core @babel/preset-env --save-dev
Copy the code
// .mocharc.js
+ require('@babel/register');
global.expect = require('chai').expect;
Copy the code
// .babelrc
+ {
+ "presets": ["@babel/preset-env", "@babel/preset- typescript "]
+}
Copy the code
Similarly, if you use TypeScript in your project, you can use TS-Node/Register to solve the problem. Because TypeScript supports ES Module conversion to CJS, Babel is not necessary for TypeScript conversion. (This assumes the default TypeScript configuration is used.)
npm install ts-node typescript --save-dev
Copy the code
// .mocharc.js
require('ts-node/register');
Copy the code
Mocha supports browser and Node testing. To test on the browser side, we need to write an HTML file with
Karma is essentially launching a web server locally and then launching an external browser to load a boot script that loads all of our source and test files into the browser and eventually executes our test case code on the browser side. So use Karma + Mocha + Chai to build a complete browser-side unit testing tool chain.
npm install karma mocha chai karma-mocha karma-chai --save-dev
npx karma init
// Which testing framework do you want to use: mocha
// Do you want to use Require.js: no
// Do you want capture any browsers automatically: Chrome
Copy the code
Here Karma initialses with Mocha support, then the second require.js is generally no unless amD-type packages are used in the business code. The third one uses Chrome as a test browser. And then I’m going to configure chai separately in the code.
// karma.conf.js module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter- frameworks: ['mocha'],
+ frameworks: ['mocha', 'chai'],
// list of files / patterns to load in the browser
files: [],
Copy the code
The frameworks function of Karma is to inject some dependencies globally. The setup here is to expose Mocha and CHAI’s testing-related tools globally for code to use. Karma just sends our files to the browser for execution, but as mentioned above our code needs to be packaged by Webpack or Browserify before it can run in the browser.
If the original code is already CJS, browserify can be used to support browser-side execution with almost zero configuration, but the real world is often more complex and we have ES6, JSX, and TypeScript to deal with, so we use WebPack.
Here is the configuration information for WebPack.
npm install karma-webpack@4 webpack@4 @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
Copy the code
// karma.conf.js module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha', 'chai'], // list of files / patterns to load in the browser files: [+ { pattern: "test/*.test.js", watched: false }
],
preprocessors: {
+ 'test/**/*.js': [ 'webpack']
},
+ webpack: {
+ module: {
+ rules: [{
+ test: /.*\.js/,
+ use: 'babel-loader'
+}]
+}
+},
Copy the code
// .babelrc
{
"presets": ["@babel/preset-env"."@babel/preset-react"]}Copy the code
Here we test a React application code as follows
// js/index.js
import React from 'react';
import ReactDOM from 'react-dom';
export function renderToPage(str) {
const container = document.createElement('div');
document.body.appendChild(container);
console.log('there is real browser');
return new Promise(resolve= > {
ReactDOM.render(<div>{ str } </div>, container, resolve);
});
}
// test/index.test.js
import { renderToPage } from '.. /js/index';
describe('renderToPage'.() = > {
it ('should render to page'.async function () {
let content = 'magic string';
await renderToPage(content);
expect(document.documentElement.innerText).to.be.contain(content); })})Copy the code
And opened the local browser
You can see that the test program is now running in a real browser.
Because graphical testing is not CI friendly, puppeteer can be used instead of Chrome.
Again, these are heavy packages, so if you don’t rely heavily on a real browser, you can use JSDOM to simulate a browser environment on the Node side.
Just a little summary of the tool chain
- The test tool chain in the Node environment can be:
mocha
+chai
+babel
- The simulated browser environment can be:
mocha
+chai
+babel
+jsdom
- The test tool chain in the real browser environment can be:
karma
+mocha
+chai
+webpack
+babel
A test pipeline often requires a combination of several tools, which is tedious to configure, and some additional tools such as unit coverage (Istanbul) and function/time simulation (sinon.js). Sometimes the fit between tools is not perfect, and the selection is time-consuming and laborious.
Jasmine alleviates this problem a little bit, but it’s not complete. Jasmine provides a testing framework that includes testing flow frameworks, assertion functions, mock tools, and more. You can think of it roughly as Jasmine = Mocha + Chai + assistive.
Let’s try jasmine’s workflow.
Initialization using NPX Jasmine Init generates a spec directory in the current directory, which contains a default configuration file
// ./spec/support/jasmine.json
{
"spec_dir": "spec"."spec_files": [
"**/*[sS]pec.js"]."helpers": [
"helpers/**/*.js"]."stopSpecOnExpectationFailure": false."random": true
}
Copy the code
So if you want to load some global configuration you can put some JS files in the Spec /helpers directory, and as the spec says, jasmine at startup will execute all the JS files in the Spec /helpers directory.
For example, we often use ES6 syntax and need to add ES6 support.
Add spec/helpers/babel.js to the file.
npm install @babel/register @babel/core @babel/preset-env --save-dev
Copy the code
// spec/helpers/babel.js
require('babel-register');
Copy the code
// .babelrc
{
"presets": ["@babel/preset-env"]}Copy the code
As with Mocha, if you need TypeScript support, you can use the following configuration
npm install ts-node typescript --save-dev
Copy the code
// spec/helpers/typescript.js
require('ts-node/register');
Copy the code
Spec_dir in the configuration file is the use case file directory for jasmine convention, and spec_files specifies the use case file format as XXX.spec.js.
With this default configuration, you can write use cases as required, for example
// ./spec/index.spec.js
import { multiple } from '.. /index.js';
describe('Multiple'.() = > {
it ('should be a function'.() = > {
expect(multiple).toBeInstanceOf(Function);
})
it ('should 7 * 2 = 14'.() = > {
expect(multiple(7.2)).toEqual(14);
})
it ('should 7 * -2 = -14'.() = > {
expect(multiple(7, -2)).toEqual(-14); })})Copy the code
Jasmine’s assertion style is very different from chai’s. Jasmine’s API is much less written than CHAI’s, and supports much clearer functionality without considering how to combine them. The JEST testing framework described below also uses this style.
nothing() toBe(expected) toBeCloseTo(expected, precisionopt) toBeDefined() toBeFalse() toBeFalsy() toBeGreaterThan(expected) toBeGreaterThanOrEqual(expected) toBeInstanceOf(expected) toBeLessThan(expected) toBeLessThanOrEqual(expected) toBeNaN() toBeNegativeInfinity() toBeNull() toBePositiveInfinity() toBeTrue() toBeTruthy() toBeUndefined() toContain(expected) toEqual(expected) toHaveBeenCalled() toHaveBeenCalledBefore(expected) toHaveBeenCalledOnceWith() toHaveBeenCalledTimes(expected) toHaveBeenCalledWith() toHaveClass(expected) toHaveSize(expected) toMatch(expected) toThrow(expectedopt) ToThrowError (Expectedopt, Messageopt) toThrowMatching(predicate) withContext(message) → {matchers}Copy the code
Run Jasmine to generate the test report
The default test report is not intuitive. If you want to provide mocha-style reports, you can install jasmine spec-Reporter and add a configuration file to the spec/helpers directory, for example, spec/helpers/reporter.js.
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
jasmine.getEnv().clearReporters(); // remove default reporter logs
jasmine.getEnv().addReporter(new SpecReporter({ // add jasmine-spec-reporter
spec: {
displayPending: true}}));Copy the code
The output use-case report is as follows
If you perform DOM level tests in Jasmine, you still need Karma or JSDOM, which is not detailed here.
So let me summarize Jasmine’s tool chain
- Test in Node environment:
Jasmine
+babel
- simulation
JSDOM
Testing:Jasmine
+JSDOM
+babel
- Real browser tests:
Karma
+Jasmine
+webpack
+babel
JEST
Jest is a complete unit testing solution from Facebook. It is a test framework, assertion library, launcher, snapshot, sandbox, and mock tool. It is also the test tool used by React. Jest and Jasmine have very similar apis, so the tools used in Jasmine can still be used naturally in Jest. You can kind of approximate it as Jest equals JSDOM initiator plus Jasmine.
While Jest provides a lot of functionality, there is no built-in ES6 support, so you still need to convert your code for different runtimes. Since Jest runs primarily in Node, you need to use babel-Jest to convert ES Module to CommonJS.
The default configuration of Jest
NPM install jest --save-dev NPX jest --init √ Would you like to use jest when running "test" script in "package.json"? . Yes √ Would you like to use Typescript for the configuration file? . No √ Choose the test environment that will be used for testing » JsDOM (browser-like) √ Do you want Jest to add coverage reports? . No √ Which provider should be used to instrument code for coverage? » Babel √ Automatically clear mock calls and instances between every test? . yesCopy the code
Add ES6 code support under Node or JSDOM
npm install jest-babel @babel/core @babel/preset-env
Copy the code
// .babelrc
{
"presets": ["@babel/preset-env"]}Copy the code
// jest.config.js // The following two actions are configured by default.+ testEnvironment: "jsdom",
+ transform: {"\\.[jt]sx? $": "babel-jest"}
}
Copy the code
Generate test reports using Jest
React and TypeScript support can also be addressed by changing the configuration of Babel
npm install @babel/preset-react @babel/preset-typescript --save-dev
Copy the code
// .babrlrc
{
"presets": ["@babel/preset-env"."@babel/preset-react"."@babel/preset-typescript"]}Copy the code
Jest was tested in a real browser environment
Jest does not support direct testing in a real browser. The default launcher only provides a JSDOM environment. Unit testing in a browser is only possible with Karma, so you can use Karma + Jest. Because Jest itself is too heavy, using Karma + Jasmine gives you basically the same effect.
Another popular E2E solution, Jest + Puppeteer, is not expanded here because E2E is not a unit test.
Jest tool chain summary
- Test in Node environment:
Jest
+babel
JSDOM
Testing:Jest
+babel
- Real browser Testing (not recommended)
E2E
Testing:Jest
+Puppeteer
A short summary
The above content introduces CHAI, Mocha, Karma, Jasmine and JEST. Each tool has its own unique tool chain. When selecting suitable test tools, you can choose them according to actual needs. Here’s a look at some methods of React unit testing.
Unit test React using Jest + Enzyme
The basic Enzyme configuration is as follows:
npm install enzyme enzyme-adapter-react-16 jest-enzyme jest-environment-enzyme jest-canvas-mock react@16 react-dom@16 --save-dev
Copy the code
// jest.config.js
{
- "testEnvironment": "jsdom",
+ setupFilesAfterEnv: ["jest-enzyme", "jest-canvas-mock"],
+ testEnvironment: "enzyme",
+ testEnvironmentOptions: {
+ "enzymeAdapter": "react16"
+},
}
Copy the code
The jest-canvas-mock package is designed to solve some problems with JSDOM triggering warnings for unimplemented behavior.
Create an Enzyme friendly environment that allows you to use the React, Shallow, mount API directly in the global scope. Enzyme also registers a number of friendly assertion functions into Jest, as shown below, for reference
toBeChecked()
toBeDisabled()
toBeEmptyRender()
toExist()
toContainMatchingElement()
toContainMatchingElements()
toContainExactlyOneMatchingElement()
toContainReact()
toHaveClassName()
toHaveDisplayName()
toHaveHTML()
toHaveProp()
toHaveRef()
toHaveState()
toHaveStyle()
toHaveTagName()
toHaveText()
toIncludeText()
toHaveValue()
toMatchElement()
toMatchSelector()
Copy the code
// js/ClassComponent.js
import React from 'react';
export default class ClassComponent extends React.PureComponent {
constructor() {
super(a);this.state = { name: 'classcomponent' };
}
render() {
return (
<div>
a simple class component
<CustomComponent />
</div>); }}// test/hook.test.js
import HookComponent from '.. /js/HookComponent';
describe('HookComponent'.() = > {
it ('test with shallow'.() = > {
const wrapper = shallow(<HookComponent id={1} />);
expect(wrapper).toHaveState('name'.'classcomponent');
expect(wrapper).toIncludeText('a simple class component');
expect(wrapper).toContainReact(<div>a simple class component</div>);
expect(wrapper).toContainMatchingElement('CustomComponent'); })})Copy the code
Enzyme provides three rendering component methods
shallow
usereact-test-renderer
Rendering a component as an in-memory object can be done easilyprops
.state
The corresponding operation object isShallowWrapper
In this mode, only the first layer of custom sub-components can be sensed, but the internal structure of custom sub-components cannot be sensed.mount
usereact-dom
The render component will create realityDOM
Node,shallow
Compared to the added ability to use nativeAPI
operationDOM
The corresponding operation object isReactWrapper
In this mode, the perception is a completeDOM
The tree.render
usereact-dom-server
To renderhtml
String, based on the static document to operate, the corresponding operation object isCheerioWrapper
.
Shallow rendering
Because the Shallow pattern only senses the first layer of custom child component components, it tends to be used only for simple component testing. For example, the following component
// js/avatar.js
function Image({ src }) {
return <img src={src} />;
}
function Living({ children }) {
return <div className="icon-living"> { children } </div>;
}
function Avatar({ user, onClick }) {
const { living, avatarUrl } = user;
return (
<div className="container" onClick={onClick}>
<div className="wrapper">
<Living >
<div className="text">Live in the</div>
</Living>
</div>
<Image src={avatarUrl} />
</div>)}export default Avatar;
Copy the code
A shallow render is not a true render, but its component life cycle goes through in its entirety.
Shallow (
The Enzyme selector supports the familiar CSS selector syntax, in which case we can do the following tests on the DOM structure
// test/avatar.test.js
import Avatar from '.. /js/avatar';
describe('Avatar'.() = > {
let wrapper = null, avatarUrl = 'abc';
beforeEach(() = > {
wrapper = shallow(<Avatar user={{ avatarUrl: avatarUrl}} / >);
})
afterEach(() = > {
wrapper.unmount();
jest.clearAllMocks();
})
it ('should render success'.() = > {
// Wrapper rendering is not null
expect(wrapper).not.toBeEmptyRender();
// The Image component render is not empty, the Image component render function is executed
expect(wrapper.find('Image')).not.toBeEmptyRender();
// Contains a node
expect(wrapper).toContainMatchingElement('div.container');
// Contains a custom component
expect(wrapper).toContainMatchingElement("Image");
expect(wrapper).toContainMatchingElement('Living');
// Shallow rendering does not contain the internal structure of the child component
expect(wrapper).not.toContainMatchingElement('img');
// Shallow render contains the children node
expect(wrapper).toContainMatchingElement('div.text');
// Shallow rendering tests the internal structure of the children node
expect(wrapper.find('div.text')).toIncludeText('Live'); })})Copy the code
Class Component props/State toHaveProp toHaveState toHaveProp toHaveState toHaveProp toHaveState But the Hook component cannot test useState.
it ('Image component receive props'.() = > {
const imageWrapper = wrapper.find('Image'); ,// For Hook components currently we can only test props
expect(imageWrapper).toHaveProp('src', avatarUrl);
})
Copy the code
Wrapper. find returns the same ShallowWrapper object, but its substructure is unexpanded and needs to be shallow render again to test the imageWrapper internal structure.
it ('Image momponent receive props'.() = > {
const imageWrapper = wrapper.find('Image').shallow();
expect(imageWrapper).toHaveProp('src', avatarUrl);
expect(imageWrapper).toContainMatchingElement('img');
expect(imageWrapper.find('img')).toHaveProp('src', avatarUrl);
})
Copy the code
You can also change the props of a component to trigger a component redraw
it ('should rerender when user change'.() = > {
const newAvatarUrl = ' ' + Math.random();
wrapper.setProps({ user: { avatarUrl: newAvatarUrl }});
wrapper.update();
expect(wrapper.find('Image')).toHaveProp('src', newAvatarUrl);
})
Copy the code
Another common scenario is event simulation, which is close to the real test scenario. Shallow event has many defects in this scenario, because the shallow event does not have the same capture and bubble process as the real event, so the corresponding callback can only be simply triggered to achieve the test purpose.
it ('will call onClick prop when click event fired'.() = > {
const fn = jest.fn();
wrapper.setProps({ onClick: fn });
wrapper.update();
// Two click events are triggered, but onClick will only be called once.
wrapper.find('div.container').simulate('click');
wrapper.find('div.wrapper').simulate('click');
expect(fn).toHaveBeenCalledTimes(1);
})
Copy the code
Some people online have summarized some of the weaknesses of the Shallow mode
shallow
Render does not perform event bubbling, whilemount
Will be.shallow
Rendering because it doesn’t create realityDOM
, so used in the componentrefs
Can’t be accessed properly if you really need to userefs
, must be usedmount
.simulate
在mount
Is more useful because it does event bubbling.
Instead, the above points point out that shallow is usually only appropriate for a desirable scenario, and that some actions that rely on browser behavior are not shallow enough to be used for real-world situations.
Mount the render
Mount renders an object structure for ReactWrapper which provides almost the same API as ShallowWrapper, with minor differences.
Some of the differences at the API level are as follows
+ getDOMNode() gets the DOM node
Detach () unmounts the React component, equivalent to unmountComponentAtNode
+ mount() to mount the component. After unmount, this method is used to remount it
+ ref(refName) gets the property on class Component instance.refs
+ setProps(nextProps, callback)
- setProps(nextProps)
- shallow()
- dive()
- getElement()
- getElements()
Copy the code
In addition, mount is more realistic because it uses the ReactDOM for rendering. In this mode, we can observe the entire DOM structure and the React component node structure.
describe('Mount Avatar'.() = > {
let wrapper = null, avatarUrl = '123';
beforeEach(() = > {
wrapper = mount(<Avatar user={{ avatarUrl}} / >);
})
afterEach(() = > {
jest.clearAllMocks();
})
it ('should set img src with avatarurl'.() = > {
expect(wrapper.find('Image')).toExist();
expect(wrapper.find('Image')).toHaveProp('src', avatarUrl);
expect(wrapper.find('img')).toHaveProp('src', avatarUrl); })})Copy the code
Event triggering problems that cannot be simulated in shallow are no longer an issue under mount.
it ('will call onClick prop when click event fired'.() = > {
const fn = jest.fn();
wrapper.setProps({ onClick: fn });
wrapper.update();
wrapper.find('div.container').simulate('click');
wrapper.find('div.wrapper').simulate('click');
expect(fn).toHaveBeenCalledTimes(2);
})
Copy the code
So, to summarize, a mount that’s shallow does not necessarily work.
Render to Render
Render internally using react-dom-server to render a string, and then through Cherrio to convert it into an in-memory structure. Return CheerioWrapper instance to render the whole DOM tree completely, but the state of the internal instance will be lost. So it’s also called Static Rendering. This kind of rendering can be carried out relatively few operations, here is not detailed, you can refer to the official documentation.
conclusion
If I had to recommend it, I would recommend Karma + Jasmine for real browsers and Jest + Enzyme for React tests that cover most scenarios in JSDOM environments. Jest allows you to mock the implementation of an NPM component, adjust the setTimeout clock, etc. These tools are also essential for unit testing. The whole unit testing technology system contains a lot of things, this paper can not cover everything, only introduces some of the most recent from our related technology system.
reference
- Medium.com/building-ib…
- medium.com/@turhan.oz/…
- www.liuyiqi.cn/2015/10/12/…
- jestjs.io/docs/en
- Blog. Bitsrc. IO/how-to – test…
- www.freecodecamp.org/news/testin…
- www.reddit.com/r/reactjs/c…
This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!