preface

Sorry, pigeon for a long time of actual combat finally come out!

This article is the React actual part of the front end automation test series. The automation test series will go from theory to practice and really lead you to learn how to use the front end automation test framework and put it into practice in your business.

Watch the entire series without using automated testing tools to improve production efficiency, please come see me!

As usual, with 200 likes, keep updating the tutorial on combining Vue and automated testing.

Check out “Hello FE” for more easy-to-understand practical tutorials.

This article is a practical tutorial, if there are mistakes in the theoretical knowledge, welcome big guy in the comment area pointed out!

After the popularization of the last article, everyone should have a certain understanding of the front-end automation test.

For those who did not read the previous article on automated testing related concepts and basic Jest grammar, click on the portal: “Don’t want to lose the General salary and Year-end Bonus? Try automated testing!”

I hope everyone can lay a good foundation before starting the actual combat part!

The actual part of the code I put in my Git warehouse: wjq990112 / Learing-React-test, welcome to click a little star ⭐️, continue to pay attention to the follow-up updates ~

Front knowledge

I hope you have a little English document reading ability

Due to the domestic practice of front-end automation is not much, the relevant introduction of the article is also very few, the Lack of Chinese information, many library classes can only read the English official documents to learn to use. Of course, in this article I will try to give you more basic and important parts of English documents.

I hope you have a littleReactbasis

If you don’t know how to use React, you can buy @God three yuan (three yuan remember to pay 🐶). If you don’t know how to use React, you can buy @God three yuan (three yuan remember to pay 🐶). React Hooks and Immutable data streams.

I hope you have a little bit of a TypeScript foundation

While I try to keep TypeScript syntax to a minimum in practice, some of my code should not be confused.

I want you to have some engineering skills

Automated testing is an engineering problem that requires some understanding. This article will involve the configuration and explanation of some babel.config.js, jest. Config. js and other configuration files.

The preparatory work

Methods a

We strongly suggest that you follow the method step by step to build our actual combat environment!

You can create your own project using the create-React-app in one of two ways:

npx create-react-app jest-react --template-typescript
Copy the code

Or:

npm install create-react-app -g
create-react-app jest-react --template-typescript
Copy the code

Once you’ve created your project, you can start experimenting with the magic of automated testing combined with React!

Method 2

If you don’t like this, you can also pull my code down from GitHub and select the corresponding tutorial branch:

# Basic Tutorial
git checkout base
Advanced tutorial: Testing - Library
git checkout advance/testing-library
# Advanced tutorial: enzyme
git checkout advance/enzyme
Copy the code

Then execute:

npm install
npm run start
Copy the code

Service run at http://localhost:3000, will automatically open the browser, service startup is complete after you can see the actual combat project interface!

Of course, the main purpose of this project is to explain automated testing, so the interface is not as beautiful. It is designed to allow you to learn front-end automation testing through real code practice. If you want to make the interface more beautiful, you can pull down the code and add some style!

However, I still recommend that you use the method step by step, it will be a little more impressive, will help you learn and understand the content of this article faster!

Basic tutorial

Component development

Once we’ve done our prep work, we can start writing the code.

As usual, take it slow the first time, get used to it, and avoid trying too hard:

Let’s write a HelloWorld first, hope it can bring us good luck in the future study!

// App.tsx
import React, { useState } from 'react';
import './App.css';

function App() {
  const [content, setContent] = useState('Hello World! ');

  return (
    <div
      className="app"// Easy to get in the test caseDOMnodedata-testid="container"
      onClick={()= >{ setContent('Hello Jack! '); }} > {content}</div>
  );
}

export default App;
Copy the code

Very simple component, click on it and it becomes Hello Jack! :

Test case writing

First, let’s think about what we need to test for our HelloWorld component.

Let’s put ourselves in the user’s shoes:

  1. See Hello World!
  2. Click on the Hello World!
  3. See Hello, Jack!

So, we need to make this process work, and we need to go through these steps:

  1. The HelloWorld component renders normally,divThe content of the tag isHello World!
  2. childrenHello World!divThe tag is clicked
  3. divOf the labelchildrenbecomeHello Jack!

Draw a flow chart:

You see, here we are using the BDD way of thinking, if you don’t know BDD, you can go back to my last article: Portal.

So, now that we’ve identified the action points we need to test, we can start writing the test code using the React Testing Library, the official default.

React was already configured for us by default when we created the project, and we didn’t need to test it manually in the basics tutorial.

When we create the project, we will find an app.test. TSX file in the SRC directory. When we open it, we will find a very simple test case:

// App.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link'.() = > {
  // The render method returns a wrapper object containing some query/fetch methods to the DOM
  // getByText: gets the DOM from the tag's text
  const { getByText } = render(<App />);
  // Get the DOM where the text matches the regex /learn react/ I
  const linkElement = getByText(/learn react/i);
  // Determine whether the DOM is in the Document
  expect(linkElement).toBeInTheDocument();
});
Copy the code

This is the simplest Demo, I may not understand the place, I have commented in the code, more detailed content or to React Testing Library documentation to get.

Run the test case

Let’s do it on the console first

npm run test
Copy the code

Here’s what you’ll find:

This is because the getByTextId API throws an exception if no corresponding DOM node is found.

Now let’s delete this section and replace it with the test case we need to write:

// App.test.tsx
import React from 'react';
import { render, fireEvent, RenderResult } from '@testing-library/react';
import App from './App';

let wrapper: RenderResult;

// Render the components before running each test case
beforeEach(() = > {
  wrapper = render(<App />);
});

describe('Should render App component correctly'.() = > {
  // Initialize the text with "Hello World!"
  test('Should render "Hello World!" correctly'.() = > {
    // getByTestId: obtain the DOM by using the attribute data-testid
    // Here we get the div tag from the HelloWorld component
    const app = wrapper.getByTestId('container');
    expect(app).toBeInTheDocument();
    // Check whether the tag is a div
    expect(app.tagName).toEqual('DIV');
    // Determine whether the div tag's text matches the regular pattern /world/ I
    expect(app.textContent).toMatch(/world/i);
  });

  // Click and the text is "Hello Jack!"
  test('Should render "Hello Jack!" correctly after click'.() = > {
    const app = wrapper.getByTestId('container');
    // fireEvent: simulates the click event
    fireEvent.click(app);
    expect(app.textContent).toMatch(/jack/i);
  });
});
Copy the code

Some of you may be starting to get confused here, but that’s ok, let’s look at our test code line by line.

let wrapper: RenderResult;

// Render the components before running each test case
beforeEach(() = > {
  wrapper = render(<App />);
});
Copy the code

BeforeEach lifecycle hook is run beforeEach test case is run. Here we render the HelloWorld component into a jsdom environment simulated on node, which is essentially a browser simulated on node.

We use wrapper variables to hold our rendered results, and then use methods encapsulated by the React Testing Library to retrieve the corresponding DOM elements.

describe('Should render App component correctly'.() = > {});
Copy the code

Add a description to the unit that tests a large test.

Of course, you can write two test cases instead. If not, Jest will default to the file name as the test unit description.

Our test cases are all written in

test('Description of test Cases'.() = > {});
Copy the code

Each test function is a test case.

The test function also has an alias, it, which you’ll see later

it('Description of test Cases'.() = > {});
Copy the code

Don’t be surprised, just know it’s test.

This part of the tutorial is a supplement to the parts that were not covered in the previous article, and those of you who don’t want to read the article don’t know why.

Now let’s run

npm run test
Copy the code

The result is this:

Isn’t it cool to see a string of green results?

Do it again

Now let’s upgrade and take a look at the React Testing Library’s official basic example:

// hidden-message.js
import React from 'react';

// NOTE: React Testing Library works with React Hooks _and_ classes just as well
// and your tests will be the same however you write your components.
function HiddenMessage({ children }) {
  const [showMessage, setShowMessage] = React.useState(false);
  return (
    <div>
      <label htmlFor="toggle">Show Message</label>
      <input
        id="toggle"
        type="checkbox"
        onChange={(e)= > setShowMessage(e.target.checked)}
        checked={showMessage}
      />
      {showMessage ? children : null}
    </div>
  );
}

export default HiddenMessage;
Copy the code
// __tests__/hidden-message.js
// these imports are something you'd normally configure Jest to import for you
// automatically. Learn more in the setup docs: https://testing-library.com/docs/react-testing-library/setup#cleanup
import '@testing-library/jest-dom';
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import HiddenMessage from '.. /hidden-message';

test('shows the children when the checkbox is checked'.() = > {
  const testMessage = 'Test Message';
  render(<HiddenMessage>{testMessage}</HiddenMessage>);

  // query* functions will return the element or null if it cannot be found
  // get* functions will return the element or throw an error if it cannot be found
  expect(screen.queryByText(testMessage)).toBeNull();

  // the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
  fireEvent.click(screen.getByLabelText(/show/i));

  // .toBeInTheDocument() is an assertion that comes from jest-dom
  // otherwise you could use .toBeDefined()
  expect(screen.getByText(testMessage)).toBeInTheDocument();
});
Copy the code

There are a lot of English notes on it, there must be friends with poor English will say: “ah, how can I understand your copy of the photo set, which is all in English!”

Don’t worry, I will translate for you!

// query* functions will return the element or null if it cannot be found
// get* functions will return the element or throw an error if it cannot be found
expect(screen.queryByText(testMessage)).toBeNull();
Copy the code

This piece of code uses an API called queryByText, which is used to find the CORRESPONDING DOM by text. But what’s the difference between queryByText and getByText?

The difference is that the QUERY * API, when called, returns null if no DOM is found, whereas the GET * API returns null if no DOM is found.

// the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
fireEvent.click(screen.getByLabelText(/show/i));
Copy the code

This section of code, after I explained above should not be unfamiliar, simulate a click event. To simulate the click event we need to find the corresponding DOM. The getByLabelText API finds the corresponding DOM through the contents of the label, passing parameters that support regular expressions.

// .toBeInTheDocument() is an assertion that comes from jest-dom
// otherwise you could use .toBeDefined()
expect(screen.getByText(testMessage)).toBeInTheDocument();
Copy the code

This code is used to determine whether or not the DOM is in the Document. The toBeInTheDocument API is the method provided by jest-dom. This method is not required, but you can also use jest’s own API, toBeDefined.

The basic part of the actual combat so much, want to see the advanced part of the students can continue to learn to watch ~

Advanced tutorial

Pop-up Project Configuration

Either way, you can create a React project based on TypeScript. Projects created using creation-React-app scaffolding already have automated testing tools introduced by default, but the scaffolding hides some of the tool’s configuration by default. If we want to pop up the configuration and manually configure it, we need to run:

npm run eject
Copy the code

The default engineering configuration will be displayed. The project directory will look like this:

README.md         node_modules      package.json      scripts
config            package-lock.json public            src
Copy the code

Will be more than the project just created when the config and scripts two folders, there are some scaffolding default project configuration files, if the engineering is not very familiar, do not mess with it!

In addition to adding two more file directories, package.json also adds a number of dependencies, as well as the configuration for Babel and jest:

  "jest": {
    "roots": [
      "<rootDir>/src"]."collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}".! "" src/**/*.d.ts"]."setupFiles": [
      "react-app-polyfill/jsdom"]."setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js"]."testMatch": [
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}"."<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"]."testEnvironment": "jest-environment-jsdom-fourteen"."transform": {
      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest"."^.+\\.css$": "<rootDir>/config/jest/cssTransform.js"."^ (? ! .*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$"."^.+\\.module\\.(css|sass|scss)$"]."modulePaths": []."moduleNameMapper": {
      "^react-native$": "react-native-web"."^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
    },
    "moduleFileExtensions": [
      "web.js"."js"."web.ts"."ts"."web.tsx"."tsx"."json"."web.jsx"."jsx"."node"]."watchPlugins": [
      "jest-watch-typeahead/filename"."jest-watch-typeahead/testname"]},"babel": {
    "presets": [
      "react-app"]}Copy the code

These are the default configurations of Babel and Jest provided by React. If you don’t understand Babel and jest, don’t change them.

Migrate the Jest/Babel configuration

It’s not very convenient to put the configuration in package.json, so if you want to change it every time you have to go to package.json, let’s pull it out separately:

Create two files in the root directory, babel.config.js jest.config.js, and copy the Babel and jest configurations from package.json to the corresponding config files:

// jest.config.js

module.exports = {
  roots: ['<rootDir>/src'].collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'.'! src/**/*.d.ts'].setupFiles: ['react-app-polyfill/jsdom'].setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'].testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}'.'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'].testEnvironment: 'jest-environment-jsdom-fourteen'.transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest'.'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js'.'^ (? ! .*\\.(js|jsx|ts|tsx|css|json)$)':
      '<rootDir>/config/jest/fileTransform.js'
  },
  transformIgnorePatterns: [
    '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$'.'^.+\\.module\\.(css|sass|scss)$'].modulePaths: [].moduleNameMapper: {
    '^react-native$': 'react-native-web'.'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
  },
  moduleFileExtensions: [
    'web.js'.'js'.'web.ts'.'ts'.'web.tsx'.'tsx'.'json'.'web.jsx'.'jsx'.'node'].watchPlugins: [
    'jest-watch-typeahead/filename'.'jest-watch-typeahead/testname']};Copy the code
// babel.config.js
module.exports = {
  presets: ['react-app']};Copy the code

Detailed Jest configuration

I guess there must be some people here who say, “Oh, how can I understand all this configuration?”

Don’t worry, let’s look at them one by one!

Babel.config.js is too simple to go into detail. Let’s focus on jest.config.js:

roots: ['<rootDir>/src'].Copy the code

Roots is used to specify the root directory of Jest. Jest will only detect and run test cases in the root directory.

collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'.'! src/**/*.d.ts'].Copy the code

There are many files in the SRC directory, but we need to generate a test coverage report, so some irrelevant files will not be counted in the coverage.

CollectCoverageFrom is used to specify the scope of test coverage statistics: all js, JSX,ts, TSX files under SRC, excluding.d.ts type declarations.

setupFiles: ['react-app-polyfill/jsdom'].Copy the code

SetupFiles are used to specify the files to prepare before creating the test environment. React-app-polyfill/jsDOM is introduced here to solve jSDOM compatibility issues.

setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'].Copy the code

The setupFilesAfterEnv is used to specify the configuration file to write for each test file after the test environment is created.

We can see that the default setuptests.ts is like this:

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
Copy the code

Introduce @testing-library/jest-dom/ extend-Expect for each test file after the test environment is created, providing jEST with more React matchers such as toHaveTextContent.

testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}'.'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'].Copy the code

TestMatch is used to configure Jest to match the rules of the test file. Here we see that the configuration item is filled in with all js, JSX,ts, TSX files in the __tests__ folder and js, JSX,ts, TSX files ending in.spec/.test.

testEnvironment: 'jest-environment-jsdom-fourteen'.Copy the code

TestEnvironment is the environment used to specify the movement of test cases.

We know that Jest runs in the Node environment, but our front-end code runs in the browser environment, so we have to use some method to simulate the browser environment in the Node environment.

You can use jest- Environment -jsdom-fourteen, the Sixteen version of the React library. You can use jest- Environment – Jsdom – Fourteen, the sixteen version of the React library.

transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest'.'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js'.'^ (? ! .*\\.(js|jsx|ts|tsx|css|json)$)':
      '<rootDir>/config/jest/fileTransform.js'
  },
Copy the code

Transform is used to configure the file processing module.

In the process of testing, we actually need to remove CSS and other static resources that are not relevant to component logic, but sometimes we need to introduce these code into our component code.

When we test, we need to specify some modules to handle/replace these files, otherwise we may not find the module problem and report an error.

This part of the code is to specify all the js, JSX, ts, the TSX use Babel – jest plug-ins do processing, all of the CSS file using the < rootDir > / config/jest/cssTransform js do processing module, All the js, JSX, ts, TSX, CSS, json file, use the < rootDir > / config/jest/fileTransform js module for processing.

Let’s go to the React module and see what the official configuration is like:

// cssTransform.js
'use strict';

module.exports = {
  process() {
    return 'module.exports = {}; ';
  },
  getCacheKey() {
    // The output is always the same.
    return 'cssTransform'; }};Copy the code

In the csStransForm. js module we can see that by default an empty module is used instead of the CSS file.

// fileTransform.js
'use strict';

const path = require('path');
const camelcase = require('camelcase');

// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html

module.exports = {
  process(src, filename) {
    const assetFilename = JSON.stringify(path.basename(filename));

    if (filename.match(/\.svg$/)) {
      // Based on how SVGR generates a component name:
      // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
      const pascalCaseFilename = camelcase(path.parse(filename).name, {
        pascalCase: true
      });
      const componentName = `Svg${pascalCaseFilename}`;
      return `const React = require('react');
      module.exports = {
        __esModule: true,
        default: ${assetFilename},
        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
          return {
            $$typeof: Symbol.for('react.element'),
            type: 'svg',
            ref: ref,
            key: null,
            props: Object.assign({}, props, {
              children: ${assetFilename}})}; })}; `;
    }

    return `module.exports = ${assetFilename}; `; }};Copy the code

As you can see from the filetransform.js module, the default configuration is to create a React SVG component and return it if the file name ends in.svg, otherwise return the file name directly.

transformIgnorePatterns: [
    '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$'.'^.+\\.module\\.(css|sass|scss)$'].Copy the code

The React official configuration is to ignore all JS, JSX, TS, TSX files in the node_modules folder, and ignore all CSS Module files.

modulePaths: [],
Copy the code

ModulePaths is used to specify the path to find modules. By default, node_modules is used. If you need to find modules in another file path, you can manually specify the file path.

moduleNameMapper: {
    '^react-native$': 'react-native-web'.'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
  },
Copy the code

ModuleNameMapper is used for module mapping. ‘^ react – native $’ :’ the react – native – web is to react native configuration, web applications can be removed, ‘^. + \ \. The module \ \. (CSS | sass | SCSS) $’ : ‘identity-obj-proxy’ maps CSS modules and converts them into key-value pairs.

moduleFileExtensions: [
    'web.js'.'js'.'web.ts'.'ts'.'web.tsx'.'tsx'.'json'.'web.jsx'.'jsx'.'node'].Copy the code

The moduleFileExtensions function configures which file extensions to look for. If it’s a React single page Web application, remove any file extensions that are not js, JSX, TS, or TSX.

watchPlugins: [
  'jest-watch-typeahead/filename'.'jest-watch-typeahead/testname'].Copy the code

WatchPlugins are used to specify Jest plugins in Watch mode. For this part of the configuration, we will use the React official recommendation. There is little need for us to change.

More configuration

In fact, Jest has a lot of very useful configuration items, if you run into problems during the testing process, you may want to try the official documentation: Jest official documentation.

start

Before writing this Demo, I thought for a moment that if it was too ugly, no one would like to watch it, so I decided to watch it well. However, MY aesthetic was not very good. After thinking about it, I finally decided to transfer the style from Mr. Dell’s TODO List, write the logic and test code by myself, and it would be better to stand on the shoulders of giants.

Here’s the effect:

I won’t go into detail on the component code, but if you want to see the component code, you can clone it in the GitHub repository and switch to advance/ Testing – Library.

Let’s focus on the part about how to write tests!

Unit Testing-Library

First, it is important to analyze what functionality each component needs to test.

If you don’t initially make it clear what functionality each component needs to test, you’re likely to overtest or miss the test branch!

  • The Header component

    1. inputThere is,valueIs empty
    2. inputCan enter
    3. inputI can enter submit
    4. inputCan be submitted after thevalueempty
  • The List component

    1. The list is empty, there are no list items, and the counter in the upper right corner exists with a value of 0
    2. The list is not empty, there are list items, the counter in the upper right corner exists and is the length of the list, the list item deletion button exists, and the list item can be deleted
    3. The list is not empty. There are list items. The counter in the upper right corner exists and is the length of the list iteminput, press Enter to modify the contents of the corresponding list item

Now that we know what functionality we need to test, we can start writing code for unit tests to see if each component works:

// Header.test.tsx
import React from 'react';
import { render, fireEvent, RenderResult } from '@testing-library/react';
import Header from '.. /.. /components/Header';

let wrapper: RenderResult;
let input: HTMLInputElement;
const addUndoItem = jest.fn();

beforeEach(() = > {
  wrapper = render(<Header addUndoItem={addUndoItem} />);
  input = wrapper.getByTestId('header-input') as HTMLInputElement;
});

afterEach(() = > {
  wrapper = null;
  input = null;
});

describe('the Header components'.() = > {
  it('Component initialization is normal'.() = > {
    / / input
    expect(input).not.toBeNull();

    // The component initializes input value to null
    expect(input.value).toEqual(' ');
  });

  it('The input box should work.'.() = > {
    const inputEvent = {
      target: {
        value: 'Learn Jest'}};// Analog input
    // The input value is the input value
    fireEvent.change(input, inputEvent);
    expect(input.value).toEqual(inputEvent.target.value);
  });

  it('The field should be submitted and cleared if I press enter.'.() = > {
    const inputEvent = {
      target: {
        value: 'Learn Jest'}};const keyboardEvent = {
      keyCode: 13
    };
    // Simulate carriage return
    // 调用 addUndoItem props 调用时参数为 input value
    // Set input value to null
    fireEvent.change(input, inputEvent);
    fireEvent.keyUp(input, keyboardEvent);
    expect(addUndoItem).toHaveBeenCalled();
    expect(addUndoItem).toHaveBeenCalledWith(inputEvent.target.value);
    expect(input.value).toEqual(' ');
  });
});
Copy the code
// List.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import List, { IList } from '.. /.. /components/List';

describe('List components'.() = > {
  it('Component initialization is normal'.() = > {
    const props: IList = {
      list: [].deleteItem: jest.fn(),
      changeStatus: jest.fn(),
      handleBlur: jest.fn(),
      valueChange: jest.fn()
    };

    const wrapper = render(<List {. props} / >);
    const count = wrapper.queryByTestId('count');
    // The counter exists and the value is 0
    expect(count).not.toBeNull();
    expect(count.textContent).toEqual('0');

    const list = wrapper.queryAllByTestId('list-item');
    // The list item is empty
    expect(list).toHaveLength(0);
  });

  it('List items should be able to be deleted'.() = > {
    const props: IList = {
      list: [{ status: 'div'.value: 'Learn Jest'}].deleteItem: jest.fn(),
      changeStatus: jest.fn(),
      handleBlur: jest.fn(),
      valueChange: jest.fn()
    };

    const wrapper = render(<List {. props} / >);
    const count = wrapper.queryByTestId('count');
    // The counter exists and the value is 1
    expect(count).not.toBeNull();
    expect(count.textContent).toEqual('1');

    const list = wrapper.queryAllByTestId('list-item');
    // The list item is not empty
    expect(list).toHaveLength(1);

    const deleteBtn = wrapper.queryAllByTestId('delete-item');
    // The delete button is not empty
    expect(deleteBtn).toHaveLength(1);
    const e: Partial<React.MouseEvent> = {};
    fireEvent.click(deleteBtn[0], e);
    // Stop event bubbling
    expect(props.changeStatus).not.toHaveBeenCalled();
    // deleteItem is called with an argument of 0
    expect(props.deleteItem).toHaveBeenCalled();
    expect(props.deleteItem).toHaveBeenCalledWith(0);
  });

  it('List items should be editable'.() = > {
    const props: IList = {
      list: [{status: 'div'.value: 'Learn Jest' },
        { status: 'input'.value: 'Learn Enzyme'}].deleteItem: jest.fn(),
      changeStatus: jest.fn(),
      handleBlur: jest.fn(),
      valueChange: jest.fn()
    };

    const wrapper = render(<List {. props} / >);
    const list = wrapper.queryAllByTestId('list-item');
    // The first item is not in the edit state and the second item is in the edit state
    expect(list[0].querySelector('[data-testid="input"]')).toBeNull();
    expect(list[1].querySelector('[data-testid="input"]')).not.toBeNull();

    // Click the first item
    fireEvent.click(list[0]);
    // changeStatus is called with the argument 0
    expect(props.changeStatus).toHaveBeenCalled();
    expect(props.changeStatus).toHaveBeenCalledWith(0);

    // The second item is input
    fireEvent.change(list[1].querySelector('[data-testid="input"]'), {
      target: { value: 'Learn Testing Library'}});ValueChange is called with parameter 1 Learn Enzyme
    expect(props.valueChange).toHaveBeenCalled();
    expect(props.valueChange).toHaveBeenCalledWith(1.'Learn Testing Library');

    // The second input box is out of focus
    fireEvent.blur(list[1].querySelector('[data-testid="input"]'));
    // handleBlur is called with an argument of 1
    expect(props.handleBlur).toHaveBeenCalled();
    expect(props.handleBlur).toHaveBeenCalledWith(1);
  });
});
Copy the code

The unit testing part of the code is basically some matchers introduced in the last article, you can go back to review: “Try front-end automation testing! (Basic)”, this part will not be explained in detail.

Integration Testing (Testing-Library)

Some of you might say, “Well, you tested the individual components to see if they worked, but what if they didn’t work together?”

If you see this and feel this way, then you have a good understanding of automated testing.

Unit tests have a default premise: if all the components of a piece of code work, then the code will work.

Just like a gear set, if all the gears in the gear set work, then the whole gear set will work.

Normally, in enterprise development, if we can achieve high unit test coverage, there is no need to write integration tests to verify that the combined code works.

But, it’s a little different here. Why?

Did you find that the above test code did not test whether the List component added this function after the Header component was input?

Therefore, we need to use the integration test to test whether the application functions properly when the two components are combined:

// App.resolved.test.tsx
import React from 'react';
import { render, fireEvent, act, RenderResult } from '@testing-library/react';
import App from '.. /.. /App';
import axios from 'axios';

jest.mock('axios');
axios.get.mockResolvedValue({
  data: {
    code: 200.data: [{status: 'div'.value: 'learning Jest'
      },
      {
        status: 'div'.value: 'learning Enzyme'
      },
      {
        status: 'div'.value: 'learning Testing - Library'}].message: 'success'}});let wrapper: RenderResult;
let headerInput: HTMLInputElement;
let count: HTMLDivElement;
let list: HTMLLIElement[];
let input: HTMLInputElement[];
let deleteBtn: HTMLDivElement[];

// Render the components before running each test case
beforeEach(async() = > {await act(async () => {
    wrapper = render(<App />);
  });
  headerInput = wrapper.getByTestId('header-input') as HTMLInputElement;
  count = wrapper.queryByTestId('count') as HTMLDivElement;
  list = wrapper.queryAllByTestId('list-item') as HTMLLIElement[];
  input = wrapper.queryAllByTestId('input') as HTMLInputElement[];
  deleteBtn = wrapper.queryAllByTestId('delete-item') as HTMLDivElement[];
});

// Reset after run
afterEach(() = > {
  wrapper = null;
  headerInput = null;
  count = null;
  list = [];
  input = [];
  deleteBtn = [];
});

describe('App component (request successful) '.() = > {
  it('Component initialization is normal'.() = > {
    / / headerInput exist
    expect(headerInput).not.toBeNull();

    // The component initializes headerInput Value to null
    expect(headerInput.value).toEqual(' ');

    // The counter exists and the value is 3
    expect(count).not.toBeNull();
    expect(count.textContent).toEqual('3');

    // The list item is not empty and has length 3
    expect(list).toHaveLength(3);

    // There are no list items in the edit state
    expect(input).toHaveLength(0);
  });

  it('List items should be added after the input box is submitted'.() = > {
    fireEvent.change(headerInput, {
      target: { value: 'Sharing Automated Testing Learning outcomes'}}); fireEvent.keyUp(headerInput, {keyCode: 13 });

    expect(count.textContent).toEqual('4');
    // This will trigger DOM changes that need to be queried again
    list = wrapper.queryAllByTestId('list-item') as HTMLLIElement[];
    expect(list).toHaveLength(4);

    // The last item is the added content
    expect(list[3]).toHaveTextContent('Sharing Automated Testing Learning outcomes');
  });

  it('List items should be reduced after deletion'.() = > {
    fireEvent.click(deleteBtn[2]);

    expect(count.textContent).toEqual('2');
    // This will trigger DOM changes that need to be queried again
    list = wrapper.queryAllByTestId('list-item') as HTMLLIElement[];
    expect(list).toHaveLength(2);
  });

  it('List items should be able to edit and submit'.() = > {
    fireEvent.click(list[2]);
    const editingItemInput = list[2].querySelector(
      '[data-testid="input"]'
    ) as HTMLInputElement;

    // The first and second items are not in the edit state. The third item is in the edit state
    expect(list[0].querySelector('[data-testid="input"]')).toBeNull();
    expect(list[1].querySelector('[data-testid="input"]')).toBeNull();
    expect(editingItemInput).not.toBeNull();

    // Enter the third item
    fireEvent.change(editingItemInput, {
      target: { value: 'Learn Testing Library'}}); expect(editingItemInput.value).toEqual('Learn Testing Library');

    // The content is changed when out of focus
    fireEvent.blur(editingItemInput);
    expect(list[2]).toHaveTextContent('Learn Testing Library');
  });
});
Copy the code

For those of you who have looked at the code carefully, you may find this code confusing:

// mock api
jest.mock('axios');
axios.get.mockResolvedValue({
  data: {
    code: 200.data: [{status: 'div'.value: 'learning Jest'
      },
      {
        status: 'div'.value: 'learning Enzyme'
      },
      {
        status: 'div'.value: 'learning Testing - Library'}].message: 'success'}});Copy the code
// async render
await act(async () => {
  wrapper = render(<App />);
});
Copy the code

This is because our application uses Axios to request local data that is rendered on the page.

In actual development, the backend interface must be invoked to retrieve or manipulate data, which leads to the important point that test code should not be intrusive to business code!

In fact, the intrusion is not just into the business code, but also into the back-end interface.

The mock API code above simulates the return value of an interface in a test environment. Since the GET method of AXIos returns a Promise, we should also call mockResolvedValue to simulate the result of the interface’s resolve state.

The async render interface is an asynchronous process. If we render in a synchronous way, the asynchronous event will be put in the event queue until the synchronized code is executed.

In this case, the test case will run out before the asynchronous event is executed, and the test may fail.

You can also create an app.reject.test. TSX file to test the state of the reject interface. (In real development, the interface might also need a friendly prompt.)

Unit Test & Integration Test (Enzyme)

Besides The Testing-Library officially recommended by React, Airbnb also launched a Testing framework Enzyme, which is also a very useful Testing framework with a slightly different design idea. Interested students can visit the GitHub repository for this project. Remember to switch to the Advance /enzyme branch.

The Enzyme code is entirely written by Mr Dell

Unit Tests (Hooks related)

When developing with React Hooks, we may extract some repetitive logic into public Hooks. Reliability is also important, and testing-library also provides us with tools for Testing React Hooks: react-hooks-testing-library

The resources

  • Dell Lee: The front end of the test lessons to learn from Jest introduction to TDD/BDD double combat
  • Jest-dom official documentation
  • React-testing-library official document
  • Enzyme official document