High-quality front-end automated testing

Front-end automated testing: Testing Library

introduction

Front-end testing:
static testing eslint, TypeScript
unit testing jest, mocha
integration testing enzyme, react-testing-library, mock
crawler ,
front-end and back-end decoupling

Why introduce automated testing

Testing allows developers to consider problems from the user's perspective and ensure that each function of the component can run normally through testing.

When writing unit tests, in most cases the component code will be adjusted repeatedly. Through constant polishing, careless considerations during development are avoided, thereby improving the quality of the component. The same is true for business modules that change infrequently.

According to the book "Google Software Testing", the definition of testing certification levels is as follows:

  • Level 1

    • Use test coverage tools.
    • Use continuous integration.
    • The test grades are small, medium, and large.
    • Create a collection of smoke tests (main process test cases).
    • Mark which tests are non-deterministic (test results are not unique).
  • Level 2

    • If any test runs with a red (failed) result it will not be published.
    • Smoke testing is required before every code submission. (Self-test, simply follow the main process)
    • The overall code coverage of various types should be greater than 50%.
    • The coverage rate of small tests should be greater than 10%.
  • Level 3

    • All important code changes are tested.
    • The coverage of small tests is greater than 50%.
    • All new important features must be verified through integration testing.
  • Level 4

    • Smoke tests are automatically run before any new code is committed.
    • The smoke test must be run within 30 minutes.
    • There is no testing for uncertainty.
    • Overall test coverage should be no less than 40%.
    • The code coverage of small tests should be no less than 25%.
    • All important functionality should be verified by integration tests.
  • Level 5

    • For each important defect fix, a test case should be added corresponding to it.

    • Actively use available code analysis tools.

    • The overall test coverage is no less than 60%.

    • Small test code coverage should be no less than 40%.

Choice of testing tools

Jest, Mocha, Testing Library and Enzyme are the most widely used tools for front-end automated testing.

Simple rules to follow

AAA mode : Arrange, Act, Assert

1. Arrangement

The orchestration phase can be divided into two parts. Render the component and get the required DOM elements .

2. Execution

After preparing the content to be tested, you can use the tool API of the testing library to perform simulation operations. For example, use React Testing Librarysimulate fireEventclick events. (This step is not necessary, you can also directly assert the content obtained by the arrangement)

fireEvent.click(mockFn)

3. Assertion

Assertion is also judgment. Use the built-in assertion statements in Jest or React Testing Library to judge the above content, and pass if it passes.

Introduction

Official website manual

The Testing Library is a lightweight testing solution for testing web pages by querying and interacting with DOM nodes (either using JsDOM / Jestemulating or in the browser). Provide query nodes in a way that is close to how users find elements on the page.

The Testing Library can be applied to all major frameworks (including React, , Angularand Vue) and can be Cypressused as the core in the testing framework.

This article explores the React Testing Library (RTL)

Thought

The Testing Library encourages testing to avoid implementation details (such as a component's internal state, internal methods, lifecycle, subcomponents) and instead emphasizes focusing on content similar to what the user actually interacts with.

The more closely the tests are written to resemble how users of the software will use them, the more confidence they can bring to the test.

core

Inquire

type

There are three ways to query elements in RTL , namely getBy, queryBy, findBy. If you want to query multiple elements, use xxxAllByinstead. The differences are as follows:

Query type 0 corresponds to 1 corresponds >1 corresponds async/await await
getBy… Report an error return Report an error no
queryBy… null return Report an error no
findBy… Report an error return Report an error yes
getAllBy… Report an error Return array Return array no
queryAllBy… [ ] Return array Return array no
findAllBy… Report an error Return array Return array yes

Generally speaking, getBy is used to query for normally existing elements (if not found, an error will be reported), queryBy is used to query for elements that do not exist (if not found, no error will be reported), and findBy is used to query asynchronous elements that need to be waited for.

Using *ByRole will read the hidden element name as "". You can consider another better query method. If you still need to use *ByRole to query, you can refer to the official manual . Problem details

priority

id、classAccording to the guiding ideology of the test library, certain priorities should be observed when using the query API (from the user's perspective, you can directly use what you see on the page as the selector instead of using selectors that the user cannot see ) .

  • regular

    • getByRole finds 特定角色elements with

    • labelgetByLabelText finds elements matching the given text

    • getByPlaceholderText finds 占位符属性elements with

    • getByText finds 文本节点elements with

    • getByDisplayValue finds valuecontrol elements with

  • semantic query

    • getByAltText finds altelements with attributes and text corresponding to alt that match the text

    • getByTitle returns titlethe element that has the attribute and matches the text corresponding to title

  • With testId

    • getByTestId (Consider avoiding meaningless attributes in the production environment, you can babel-plugin-react-remove-properties remove data-testthe test auxiliary selector)

      // .bablerc
      {
        "env": {
          "production": {
            "plugins": ["react-remove-properties"]
          }
        }
      }
      
practice

Since Test Library emphasizes testing from the user's perspective, it prefers to use Accessibility elements on the DOM. (But sometimes it is more efficient to only use the style selector or directly use the style selector. It can only be said to find the balance between philosophy and life )

Check out the following points for the ARIA role method corresponding to the HTML element :

1. Use Chrome Developer Tools

Elements > Accessibility

2. Using logRolesthe API provided by RTL

import {
    
     render, logRoles } from '@testing-library/react';

test('find ARIA role', () => {
    
    
	const {
    
     container } = render(<Component />);
	logRoles(container);
})

3. Use third-party plug-ins

Accessing elements is even easier using a Google Chrome plug-in.

Testing library: which query

Testing Playground

User action

With fireEvent, interactive events generated by actual users can be simulated.

There is also a high-level library under the Testing Library @testing-library/user-eventthat provides more interactive events than fireEvent.

fireEvent(node: HTMLElement, event: Event)

fireEventCorresponding eventMapevent set attributes

Commonly used assertions

RTL extends jest's API and defines its own assertion functions. All assertion functions are included in @testing-library/jest-domthe package. For details, see: Built-in assertion library

toBeDisabled
toBeEnabled
toBeEmptyDOMElement
toBeInTheDocument
toBeInvalid
toBeRequired
toBeValid
toBeVisible
toContainElement
toContainHTML
toHaveAttribute
toHaveClass
toHaveFocus
toHaveFormValues
toHaveStyle
toHaveTextContent
toHaveValue
toHaveDisplayValue
toBeChecked
toBePartiallyChecked
toHaveDescription

Is

The use RTLrequires Jest, so let's first learn the basic use of Jest in a "wild way" .

introduce

Jest is a testing framework RTLand a testing solution. RTLThe meaning of existence is to write tests in a more beautiful and powerful way.

assertion matcher

judge true or false
  1. toBeNull

  2. toBeUndefined

  3. toBeDefined

  4. toBeTruthy

  5. toBeFalsy

Number related
  1. toBeGeaterThan is greater than a certain number

  2. toBeGeaterThanOrEqual is greater than or equal to

  3. toBeLessThan is less than a certain number

  4. toBeLessThanOrEqual is less than or equal to

  5. toBeCloseTo equality judgment of floating point numbers

not modifier

match opposite of expected

string matching
  1. toMatch
Array, collection related
  1. toContain determines whether an array or collection contains an element

  2. toHaveLength determines the length of the array

Function related
  1. toHaveBeenCalled determines whether mockFunc is called

  2. toHaveBeenCalledWith('_test_param') calls whether the parameter received is _test_param

  3. The number of toHaveBeenCalledTimes(2) calls

  4. toHaveReturned whether there is a return value

  5. toHaveReturnedWith('_test_return') whether the return value is _test_return

Abnormally related
  1. toThrow determines whether the thrown exception meets expectations

Mock function

Jest's three commonly used Mock function APIs are jest.fn(), jest.spyOn(), jest.mock().

In unit testing, more often than not, you do not need to care about the execution process and results of the internal calling methods. You only need to confirm whether they are called correctly.

The Mock function provides three features:

  • Capture function calls
  • Set function return value
  • Change the internal implementation of a function

1. Jest.fn()

Default returns undefinedas the return value.

const test_click = jest.fn(1,2,3);

expect(test_click).toHaveBeenCalledTimes(1) // 测试是否调用了 1 次
expect(test_click).toHaveBeenCalledWith(1,2,3) // 测试是否调用了 1 次

You can also customize the return value and implement it internally

// 自定义返回值
let customFn = jest.fn().mockReturnValue('default');
expect(customFn()).toBe('default');

// 自定义内部实现
let customInside = jest.fn((num1, num2) => num1 + num2);
expect(customInside(10, 10)).toBe(20);

// 返回 Promise
test('jest 返回 Promise', async() => {
    let mockFn = jest.fn().mockResolveValue('promise');
    let result = await mockFn();
    expect(result).toBe('promise');
    expect(Object.prototype.toString.call(mockFn())).toBe('[object Promise]')
})

Grouping and hooks

Use describekeywords to wrap tests.

describe('分别测试', () => {
    
    
  /** testItem **/
  /** hook 函数 **/
}

It should be noted that the running order of the describe block is always before the execution order of the test function. doubt?

// describe 块内程序的执行顺序()
describe('1', () => {
    
    
  console.log('1');
  describe('2', () => {
    
     console.log('2'); });
  describe('3', () => {
    
    
    console.log('3');
    describe('4', () => {
    
     console.log('4'); })
    describe('5', () => {
    
     console.log('5'); })
  })
  describe('6', () => {
    
     console.log('6'); })
})
describe('7', () => {
    
    
  console.log('7');
  it('(since there has to be at least one test)', () => {
    
     console.log('8') });
})

// 测试结果,顺序 1-8

You can use the hook function provided by jest to prepare data for the singleton.

The hook functions provided by Jest are:

  1. beforeEach

  2. beforeAll

  3. afterEach

  4. afterAll

Need to pay attention to describethe hook function used externally and the execution order of the hook function internally:

beforeAll(() => console.log('outside beforeAll'));
beforeEach(() => console.log('outside beforeEach'));
afterAll(() => console.log('outside afterAll'));
afterEach(() => console.log('outside afterEach'));

describe('', () => {
    
    
	beforeAll(() => console.log('intside beforeAll'));
    beforeEach(() => console.log('intside beforeEach'));
    afterAll(() => console.log('intside afterAll'));
    afterEach(() => console.log('intside afterEach'));

    test('test1', () => console.log('test1 run'));
    test('test2', () => console.log('test2 run'));
})

/** 结果
outside beforeAll
inside beforeAll

outside beforeEach
inside beforeEach
test1 run
inside afterEach
outside afterEach

outside beforeEach
inside beforeEach
test2 run
inside afterEach
outside afterEach

inside afterAll
outside afterAll
**/

Test async modules

Pass in the done parameter in the function that includes the test , and Jest will end the test after the done callback function is executed. If done is never called, the test case fails and a timeout error is output.

import timeout from './timeout'

test('测试timer', (done) => {
    
    
    timeout(() => {
    
    
        expect(2+2).toBe(4)
        done()
    })
})

If the asynchronous function returns a Promise, you can return the Promise directly, and Jest will wait for the resolve status of the Promise. If you expect a Promise to be Rejected, you need to use .catcha method and add expect.assertionsa certain number of assertions to verify that it is called.

test('the data is peanut butter', () => {
    
    
  return fetchData().then(data => {
    
    
    expect(data).toBe('peanut butter');
  });
});

test('the fetch fails with an error', () => {
    
    
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

test('the data is peanut butter', () => {
    
    
  return expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', () => {
    
    
  return expect(fetchData()).rejects.toMatch('error');
});

With async/await

test('the data is peanut butter', async () => {
    
    
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
    
    
  expect.assertions(1);
  try {
    
    
    await fetchData();
  } catch (e) {
    
    
    expect(e).toMatch('error');
  }
});

When testing Reactcomponents, you often encounter useEffectscenarios where State is updated in or asynchronously updated. In this case, the processing of test assertions requires the use of ActAPIs. Case description and usage

Configuration file

// 一下列举常用配置项目,所有配置属性访问官网手册 https://jestjs.io/docs/configuration

// 默认地,Jest 会运行所有的测试用例然后产出所有的错误到控制台中直至结束。
// bail 配置选项可以让 Jest 在遇到第一个失败后就停止继续运行测试用例,默认值 0
bail: 1,
// 每次测试前自动清除模拟调用和实例,相当于每次测试前都调用 jest.clearAllMocks,默认 false
clearMocks: false,
// 指示在执行测试时是否应收集覆盖率信息,通过使用覆盖率收集语句改造所有已执行文件,所以开启后可能会显著减慢
// 测试速度,默认 false
collectCoverage: false,
// 指定测试覆盖率统计范围,使用 glob 模式匹配,默认 undefined
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
// 指定忽略匹配的覆盖范围,默认 ["/node_modules"]
coveragePathIgnorePatterns: ["/node_modules/"],
// 配置一组在所有测试环境中可用的全局变量, 默认 {}
globals: {},
// 用于给模块路径映射到不同的模块, 默认 null
moduleNameMapper: {
  "\\.(css|less|scss|sss|styl)$": "jest-css-modules" | "identity-obj-proxy"
}
// 指定 Jest 根目录,Jest 只会在根目录下测试用例并运行
rootDir: './',
// 设置 Jest 搜索文件的目录路径列表,默认 []
roots: [],
// 指定创建测试环境前的准备文件,针对每个测试文件都会运行一次
setupFiles: ['react-app-polyfill/jsdom'],
// 指定测试环境创建完成后为每个测试文件编写的配置文件
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
// 配置 Jest 匹配测试文件的规则, 使用 glob 规则
testMatch: [
  '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
  '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
// 用于指定测试用例运行环境,默认 node
testEnvironment: 'jsdom',
// 配置文件处理模块应该忽略的文件
transformIgnorePatterns: [
  '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
  '^.+\\.module\\.(css|sass|scss)$'
],
// 指定转换器: js jsx ts tsx 使用 babel-jest 进行转换 css 使用 cssTransform.js 进行转换 其他文件使用 fileTransform.js
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'
},
// 转化器忽略文件: node_modules 目录下的所有 js jsx ts tsx cssModule 中的所有 css sass scss
transformIgnorePatterns: [
  '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
  '^.+\\.module\\.(css|sass|scss)$'
],
// 测试超时时间,默认 5000 毫秒
testTimeout: 5000


Practice and Debugging

When a test fails, you should first check whether running the test case alone failed. Just testchange to test.only.

// 编写一条测试
test(describe, () => {
    
    
    render(<Component />);
    expect();
})

// describe 把多条测试包裹起来进行分组
describe(describe, () => {
    
    
  test('test1', fn);
  test('test2', fn);
  test.only('test3', fn); // 再次运行测试,便只会对该单列进行测试
})

When you need to view the found Dom element in the terminal, use prettyDOM to include the element, and you can browse the results close to the HTML structure. When null is passed, prettyDOM returns the rendering result of the entire document.

const div = container.querySelector('div');
console.log(prettyDOM(div));

DemoDemo

Repository: React Testing Library

Project preparation

npm i create-react-app -g // 全局安装脚手架

create-react-app React-Testing-Library // 创建项目

yarn add @types/react @types/react-dom --dev //  基于 TypeScript

yarn add axios // 使用 axios 处理异步请求

Projects created using create-react-appscaffolding already use the Testing Library as the test solution by default, but the scaffolding hides some configurations of the tool by default. If you want to pop up the configuration and configure it manually, run it npm run eject. After the engineering configuration pops up, you only need to pay attention jestto bablethe two configurations. First add these two files jest.config.jsand in the root directory, and then migrate the configuration of the corresponding location in . (jest.config.js is redefined according to the configuration instructions, questions? )bable.config.jspackage.json

// jest.config.js
const Config = {
    
    
  bail: 1,
  clearMocks: true,
  coverageProvider: 'v8',
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
  rootDir: './',
  setupFilesAfterEnv: ['<rootDir>/src/tests/setupTests.js'],
  testEnvironment: 'jsdom',
  testMatch: ['<rootDir>/src/**/__tests__/**/*.[jt]s?(x)'],
  testPathIgnorePatterns: [
    '\\\\node_modules\\\\',
  ],
  moduleNameMapper: {
    
    
    "\\.(css|less|scss|sss|styl)$": "identity-obj-proxy"
  }

};

module.exports = Config;

Create teststhe folder, add setupTests.jsthe files, and then jest.config.jsadd the setupFilesAfterEnv configuration to override it.

// setupTest.js
// 导入 extend-expect 目的是 rtl 的一些断言需要建立在这个库上,如 toBeInTheDocument
import '@testing-library/jest-dom/extend-expect';

// jest.config.js
setupFilesAfterEnv: ['<rootDir>/src/tests/setupTests.js']

Create a new componentsfolder and add Headertwo Listcomponents.

Header: Enter toDo content in the input box and press Enter to add it to Listthe component.

List: A list showing all ToDo items, which can be edited or deleted with one click.

(The specific component functional structures are not listed here one by one)

Conduct testing

demand analysis
  • Header component

    • inputThe initial input box is empty
    • inputCan input content
    • inputEnter to submit content
    • inputThe input box is left blank after submission
  • List component

    • The list is empty and the counter value in the upper right corner is 0

    • The list is not empty. The counter in the upper right corner exists and the value is the length of the list. The delete button for the list item exists. Click to delete it.

    • The list is not empty. Click the content of the list item to modify it. Press Enter to save the modified content.

test writing

Add a directory under the corresponding component __tests__and create *.test.[jt]s?(x)a test case in the format.

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

import Header from '../index';

let inputNode: HTMLInputElement;
const addUndoItem = jest.fn();
const inputEvent = {
  target: {
    value: 'new todo'
  }
}

beforeEach(() => {
  render(<Header addUndoItem={addUndoItem} />);
  inputNode = screen.getByRole('textbox') as HTMLInputElement;
});

describe('测试 Header 组件', () => {
  it('初始渲染时输入框为空', () => {
    expect(inputNode).toBeInTheDocument();
    expect(inputNode.value).toEqual('');
  })

  it('输入框能输入内容', () => {
    fireEvent.change(inputNode, inputEvent);
    expect(inputNode.value).toEqual('new todo');
  })

  it('输入框回车提交内容并将输入框置空', () => {
    fireEvent.change(inputNode, inputEvent);
    const enterEvent = {
      keyCode: 13
    }
    fireEvent.keyUp(inputNode, enterEvent);
    expect(addUndoItem).toHaveBeenCalledTimes(1);
    expect(inputNode.value).toEqual('');
  })
})

// list.test.tsx
import React from 'react';
import { render, screen, fireEvent, within } from '@testing-library/react';

import List, { IListProps } from '../index';

const props: IListProps = {
  list: [{
    value: 'list item'
  }],
  deleteItem: jest.fn(),
  valueChange: jest.fn(),
  handleFinish: jest.fn()
}
let Counter: HTMLElement;
let listItemNode: HTMLElement[];

beforeEach(() => {
  render(<List {...props}/>);
  listItemNode = screen.queryAllByRole('listitem') as HTMLElement[];
});

describe('测试 List 组件', () => {
  it('初始渲染', () => {
    // 设置空白数据
    let {
      deleteItem,
      valueChange,
      handleFinish
    } = props;
    let blank = {
      list: [],
      deleteItem,
      valueChange,
      handleFinish
    }
    // 此处重新渲染从中取出 container 容器
    const { container } = render(<List {...blank}/>);
    const li = container.querySelector('li');
    // 初始渲染时列表项内容为空,计数器值为 0
    expect(li).toBeNull();
    Counter = screen.getByText(/0/i);
    expect(Counter).toBeInTheDocument();
  })

  it('删除列表项', () => {
    // 列表项不为空时,右上角计数器存在且值为列表长度
    expect(listItemNode).toHaveLength(1);
    Counter = screen.getByText(/1/i);
    expect(Counter).toBeInTheDocument();
    // 列表项删除按钮存在,点击将其删除
    let deleteBtn = listItemNode[0].querySelector('div') as HTMLElement;
    expect(deleteBtn).not.toBeNull();
    fireEvent.click(deleteBtn);
    expect(props.deleteItem).toHaveBeenCalledTimes(1);
  })

  it('编辑列表项', () => {
    // 点击列表项后可将其内容修改
    fireEvent.click(listItemNode[0]);
    let editInput = within(listItemNode[0]).getByRole('textbox') as HTMLInputElement;
    const editValue = {
      target: {
        value: 'edit todo'
      }
    }
    // 检查修改回调调用次数
    fireEvent.change(editInput, editValue);
    expect(props.valueChange).toHaveBeenCalledTimes(1);
    const keyUp = {
      keyCode: 13
    }
    // handleFinish 在输入框失去焦点和回车确认时触发事件
    fireEvent.keyUp(editInput, keyUp);
    // 注意,toHaveBeenCalledTimes 断言是记录历史调用次数,
    // 所有在这里想要判断 valueChange 不被调用,参数应该为 1 而不是 0
    expect(props.valueChange).toHaveBeenCalledTimes(1);
    // change 直接修改 input 的 value 值,不触发失焦事件
    expect(props.handleFinish).toHaveBeenCalledTimes(1);
  })
})

What needs to be noted when writing tests is that if the update action of the component is handled externally, you cannot expect to get the same feedback as in the business after performing certain operations. You should verify its function by testing the response of the callback function. feasibility.

Other scenes

Asynchronous operations

For the response event or that the test needs to wait for Promise, use awaitor thento process.

// 场景一
// 点击按钮后异步更新其 textContent 文本,可以借助 findBy 异步查询 API 查找需要等待更新的元素
const button = screen.getByRole('button', {name: 'Click Me'})
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')

// 场景二
// 需要等待回调函数的结果,使用 waitFor 对断言进行判断
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))

// 场景三
// 需要等待从 DOM 中删除元素
waitForElementToBeRemoved(document.querySelector('div.getOuttaHere')).then(() =>
  console.log('Element no longer in DOM'),
)
Rudux test

For particularly complex ones redux, you can choose to use basic unit tests for reducerand . effectIn more scenarios, redux connectuse the components of 集成测试. It can be passed explicitly mock storeor wrapped using Redux Providera component.

Another test solution is to test the components separately , import the components that reduxare not connected separately , and use the method to simulate their test responsiveness.connectMockdispatch

Official website case

Testing Redux-connected components

// 建立带有 redux 测试的自定义 render, 后续测试集成组件便可以使用该自定义 render
// test-utils.jsx
import React from 'react'
import {
    
     render as rtlRender } from '@testing-library/react'
import {
    
     createStore } from 'redux'
import {
    
     Provider } from 'react-redux'
// Import your own reducer
import reducer from '../reducer'

function render(
  ui,
  {
     
     
    initialState,
    store = createStore(reducer, initialState),
    ...renderOptions
  } = {
     
     }
) {
    
    
  function Wrapper({
     
      children }) {
    
    
    return <Provider store={
    
    store}>{
    
    children}</Provider>
  }
  return rtlRender(ui, {
    
     wrapper: Wrapper, ...renderOptions })
}

// re-export everything
export * from '@testing-library/react'
// override render method
export {
    
     render }

Question collection

  • formatMessage not initialized yet, you should use it after react app mounted #2156.

    Reference 1

    Reference 2

  • Could not find required intl object. needs to exist in the component ancestry. Using default message as fallback

References

Testing Library official website documentation

React Testing library usage summary

Jest official website documentation

Guess you like

Origin blog.csdn.net/nihaio25/article/details/129200985