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:

  1. Foo is a string ->expect(foo).to.be.a('string')
  2. Foo string contains ‘bar’ ->expect(foo).to.include('bar')
  3. 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

  1. NodeThe environment is naturally supportiveES Module (node version >= 15)
  2. usebabelThe 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

  1. Test in Node environment:Jasmine + babel
  2. simulationJSDOMTesting:Jasmine + JSDOM + babel
  3. 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
  • JSDOMTesting:Jest + babel
  • Real browser Testing (not recommended)
  • E2ETesting: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

  • shallowusereact-test-rendererRendering a component as an in-memory object can be done easilyprops.stateThe corresponding operation object isShallowWrapperIn this mode, only the first layer of custom sub-components can be sensed, but the internal structure of custom sub-components cannot be sensed.
  • mountusereact-domThe render component will create realityDOMNode,shallowCompared to the added ability to use nativeAPIoperationDOMThe corresponding operation object isReactWrapper In this mode, the perception is a completeDOMThe tree.
  • renderusereact-dom-serverTo renderhtmlString, 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 (
) is used to perceive the structure as follows. Notice that the children of div.text as a Living component can be detected, but the internal structure of Living cannot be perceived.

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

  1. shallowRender does not perform event bubbling, whilemountWill be.
  2. shallowRendering because it doesn’t create realityDOM, so used in the componentrefsCan’t be accessed properly if you really need to userefs, must be usedmount.
  3. simulatemountIs 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

  1. Medium.com/building-ib…
  2. medium.com/@turhan.oz/…
  3. www.liuyiqi.cn/2015/10/12/…
  4. jestjs.io/docs/en
  5. Blog. Bitsrc. IO/how-to – test…
  6. www.freecodecamp.org/news/testin…
  7. 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!