Jest是目前前端工程化下單元測試火熱的技術棧,而Enzyme的支持提供了Jest測試React業務、組件的能力,下面來介紹一下React組件測試的一些實際場景。
"enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-to-json": "^3.3.5", "jest": "^28.1.1", "jest-less-loader": "^0.1.2", "jsdom": "^19.0.0", //解決mount渲染組件失敗的BUG,具體見上文 "ts-jest": "^28.0.5",
由于enzyme的配置在每次需要測試組件時都需要加入,因此配置setup.js后在每次測試組件中提前引入是不錯的選擇。
setup.js:
import Enzyme from 'enzyme';import Adapter from 'enzyme-adapter-react-16';const jsdom = require('jsdom');//解決無法mount渲染組件的問題const { JSDOM } = jsdom;const { window } = new JSDOM('');const { document } = new JSDOM(``).window;global.document = document;global.window = window;//初始化配置Enzyme.configure({ adapter: new Adapter(),});export default Enzyme;
jest.config.js配置:
module.exports = { transform: { '^.+//.(ts|tsx|js|jsx)?$': 'ts-jest', '//.(less|css)$': 'jest-less-loader', // 支持less }, testRegex: '(/__tests__/.*|(//.|/)(test|spec))//.(jsx?|tsx?)$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],};
在為組件添加prop傳值之前,可配置一個基礎的 mountTest.tsx 來對組件進行一個基礎渲染掛載測試,測試通過后在進行復雜情況下的測試。
mountTest.tsx
import React from 'react';import { mount } from 'enzyme';// 此處Component的類型存在疑問,待完善export default function mountTest(Component: React.ComponentType<any" data-textnode-index-1701226829723="160" data-index-1701226829723="1307" data-index-len-1701226829723="1307" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> | React.ComponentType) { describe(`mount and unmount`, () =" data-textnode-index-1701226829723="167" data-index-1701226829723="1369" data-index-len-1701226829723="1369" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { it(`component could be updated and unmounted without errors`, () =" data-textnode-index-1701226829723="173" data-index-1701226829723="1442" data-index-len-1701226829723="1442" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { const wrapper = mount(<Component /" data-textnode-index-1701226829723="177" data-index-1701226829723="1485" data-index-len-1701226829723="1485" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(() =" data-textnode-index-1701226829723="180" data-index-1701226829723="1505" data-index-len-1701226829723="1505" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { wrapper.setProps({}); wrapper.unmount(); }).not.toThrow(); }); });}
這里拿Button按鈕舉例,具體Button組件可在http://react-view-ui.com:92/#/common/button參考,底部描述了組件的API能力。
圖片
先看一下Button組件的整體測試文件,我一共分成了4組測試用例(不包含mountTest基礎測試)。
Button.test.tsx
import React from 'react';import Button from '../../Button/index';import Enzyme from '../setup';import mountTest from '../mountTest';import { act } from 'react-dom/test-utils';const { shallow, mount } = Enzyme;mountTest(Button);describe(`button`, () =" data-textnode-index-1701226829723="230" data-index-1701226829723="2025" data-index-len-1701226829723="2025" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { it('button children show correctly', () =" data-textnode-index-1701226829723="236" data-index-1701226829723="2071" data-index-len-1701226829723="2071" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //按鈕文字內容測試 const component = shallow(<Button" data-textnode-index-1701226829723="242" data-index-1701226829723="2125" data-index-len-1701226829723="2125" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>testButton</Button" data-textnode-index-1701226829723="243" data-index-1701226829723="2144" data-index-len-1701226829723="2144" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); const button = component.find('.button'); const p = button.find('button'); expect(p.text()).toBe('testButton'); }); it('click callback correctly', () =" data-textnode-index-1701226829723="248" data-index-1701226829723="2310" data-index-len-1701226829723="2310" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //按鈕點擊回調測試 const mockFn = jest.fn(); const component = shallow(<Button handleClick={mockFn} /" data-textnode-index-1701226829723="253" data-index-1701226829723="2416" data-index-len-1701226829723="2416" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); const button = component.find('.button'); button.simulate('click'); const mockFnCallLength = mockFn.mock.calls.length; expect(mockFnCallLength).toBe(0); act(() =" data-textnode-index-1701226829723="270" data-index-1701226829723="2596" data-index-len-1701226829723="2596" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測禁用按鈕 component.setProps({ disabled: true, }); }); button.simulate('click'); expect(mockFn.mock.calls.length).toBe(mockFnCallLength); }); it('button type set show correctly color', () =" data-textnode-index-1701226829723="289" data-index-1701226829723="2820" data-index-len-1701226829723="2820" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試按鈕type被賦值className const component = mount(<Button type="primary" /" data-textnode-index-1701226829723="299" data-index-1701226829723="2901" data-index-len-1701226829723="2901" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(component.find('button').hasClass('primary')).toBe(true); }); it('button loading show correctly', () =" data-textnode-index-1701226829723="312" data-index-1701226829723="3019" data-index-len-1701226829723="3019" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試加載按鈕顯示 const component = mount(<Button type="primary" loading /" data-textnode-index-1701226829723="322" data-index-1701226829723="3096" data-index-len-1701226829723="3096" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(component.find('loading1')).not.toBeUndefined(); });});
從代碼中可以看到,初始化配置一共有如下代碼:
import React from 'react';import Button from '../../Button/index';import Enzyme from '../setup';import mountTest from '../mountTest';import { act } from 'react-dom/test-utils';const { shallow, mount } = Enzyme;mountTest(Button);
主要功能:引入必要的包、引入測試組件、引入組件渲染方式,這是是shallow和mount兩種,并在最后優先進行了組件基礎渲染測試。
第一組測試用例測試了Button按鈕的文字顯示正確性,是通過jest的find方法查詢到Button按鈕的DOM元素進行判斷;之后設置了組件的disabled屬性,再次進行點擊測試
it('button children show correctly', () =" data-textnode-index-1701226829723="372" data-index-1701226829723="3612" data-index-len-1701226829723="3612" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //按鈕文字內容測試 const component = shallow(<Button" data-textnode-index-1701226829723="378" data-index-1701226829723="3666" data-index-len-1701226829723="3666" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>testButton</Button" data-textnode-index-1701226829723="379" data-index-1701226829723="3685" data-index-len-1701226829723="3685" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); const button = component.find('.button'); const p = button.find('button'); expect(p.text()).toBe('testButton');});
第二組測試用例測試了按鈕的交互,在渲染組件之后,捕捉到按鈕的DOM,并自定義了mockFn函數傳遞給實際Button組件后進行回調測試,Button我在點擊時是沒有傳參的,因此回調參數長度為0
it('click callback correctly', () =" data-textnode-index-1701226829723="389" data-index-1701226829723="3943" data-index-len-1701226829723="3943" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //按鈕點擊回調測試 const mockFn = jest.fn(); const component = shallow(<Button handleClick={mockFn} /" data-textnode-index-1701226829723="398" data-index-1701226829723="4049" data-index-len-1701226829723="4049" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); const button = component.find('.button'); button.simulate('click'); const mockFnCallLength = mockFn.mock.calls.length; expect(mockFnCallLength).toBe(0); act(() =" data-textnode-index-1701226829723="415" data-index-1701226829723="4229" data-index-len-1701226829723="4229" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測禁用按鈕 component.setProps({ disabled: true, }); }); button.simulate('click'); expect(mockFn.mock.calls.length).toBe(mockFnCallLength);});
第三組測試用例對Button按鈕類型進行了測試,傳遞了type:primary,并對渲染后的組件進行判斷是否有primary的類名
it('button type set show correctly color', () =" data-textnode-index-1701226829723="435" data-index-1701226829723="4514" data-index-len-1701226829723="4514" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試按鈕type被賦值className const component = mount(<Button type="primary" /" data-textnode-index-1701226829723="445" data-index-1701226829723="4595" data-index-len-1701226829723="4595" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(component.find('button').hasClass('primary')).toBe(true);});
第四組測試用例對loading Button進行了測試,同樣也是檢查類名的形式,與第三組測試用例類似
it('button loading show correctly', () =" data-textnode-index-1701226829723="459" data-index-1701226829723="4759" data-index-len-1701226829723="4759" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試加載按鈕顯示 const component = mount(<Button type="primary" loading /" data-textnode-index-1701226829723="469" data-index-1701226829723="4836" data-index-len-1701226829723="4836" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(component.find('loading1')).not.toBeUndefined(); });
這就是我對Button的測試。
由于Button組件本身功能比較簡單,可擴展性有限,作為第一個組件案例進行舉例。
接下來對Avatar組件進行測試。
圖片
還是先上測試源碼。
Avatar.test.tsx:
import React, { ReactNode } from 'react';import ReactDOM from 'react-dom';import Avatar from '../../Avatar/index';import AvatarGroup from '../../Avatar/group';import { CameraOutlined } from '@ant-design/icons';import Enzyme from '../setup';import mountTest from '../mountTest';import { act } from 'react-dom/test-utils';const { mount } = Enzyme;let container: HTMLDivElement | null;mountTest(Avatar);describe('Avatar', () =" data-textnode-index-1701226829723="539" data-index-1701226829723="5435" data-index-len-1701226829723="5435" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試前準備容器 beforeEach(() =" data-textnode-index-1701226829723="545" data-index-1701226829723="5466" data-index-len-1701226829723="5466" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { container = document.createElement('div'); document.body.appendChild(container); }); //測試后刪除容器 afterEach(() =" data-textnode-index-1701226829723="560" data-index-1701226829723="5588" data-index-len-1701226829723="5588" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { document.body.removeChild(container as HTMLDivElement); container = null; }); it('test avatar children content show correctly', () =" data-textnode-index-1701226829723="575" data-index-1701226829723="5732" data-index-len-1701226829723="5732" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像文本顯示 let contextText: string | ReactNode = 'test'; const component = mount(<Avatar" data-textnode-index-1701226829723="588" data-index-1701226829723="5833" data-index-len-1701226829723="5833" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>{contextText}</Avatar" data-textnode-index-1701226829723="589" data-index-1701226829723="5855" data-index-len-1701226829723="5855" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(component.find('.text-ref').text()).toEqual('test'); const imgSrc = 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png'; act(() =" data-textnode-index-1701226829723="605" data-index-1701226829723="6059" data-index-len-1701226829723="6059" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { contextText = <img src={imgSrc}" data-textnode-index-1701226829723="606" data-index-1701226829723="6099" data-index-len-1701226829723="6099" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="606" data-index-1701226829723="6105" data-index-len-1701226829723="6105" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>; }); expect(component.find('img')).toBeDefined(); }); it('test avatar group correctly', () =" data-textnode-index-1701226829723="614" data-index-1701226829723="6207" data-index-len-1701226829723="6207" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像樣式 const component = ( <AvatarGroup size={50} groupStyle={{ margin: '0 10px' }}" data-textnode-index-1701226829723="622" data-index-1701226829723="6307" data-index-len-1701226829723="6307" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <Avatar style={{ background: 'rgb(20, 169, 248)' }} shape="square"" data-textnode-index-1701226829723="631" data-index-1701226829723="6382" data-index-len-1701226829723="6382" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> View </Avatar" data-textnode-index-1701226829723="633" data-index-1701226829723="6413" data-index-len-1701226829723="6413" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <Avatar style={{ background: 'rgb(51, 112, 255)' }}" data-textnode-index-1701226829723="642" data-index-1701226829723="6473" data-index-len-1701226829723="6473" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>React</Avatar" data-textnode-index-1701226829723="642" data-index-1701226829723="6487" data-index-len-1701226829723="6487" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <Avatar style={{ background: 'rgb(0, 208, 184)' }}" data-textnode-index-1701226829723="651" data-index-1701226829723="6546" data-index-len-1701226829723="6546" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>UI</Avatar" data-textnode-index-1701226829723="651" data-index-1701226829723="6557" data-index-len-1701226829723="6557" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> </AvatarGroup" data-textnode-index-1701226829723="652" data-index-1701226829723="6577" data-index-len-1701226829723="6577" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> ); act(() =" data-textnode-index-1701226829723="654" data-index-1701226829723="6596" data-index-len-1701226829723="6596" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { ReactDOM.render(component, container); }); const avatarStyleList = [ { background: 'rgb(20, 169, 248)', content: 'View', }, { background: 'rgb(51, 112, 255)', content: 'React', }, { background: 'rgb(0, 208, 184)', content: 'UI', }, ]; const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement; expect(groupDom.childElementCount).toBe(3); const avatars = Array.from((container as HTMLDivElement).querySelectorAll('.avatar')); avatars.forEach((avatar, index) =" data-textnode-index-1701226829723="708" data-index-1701226829723="7191" data-index-len-1701226829723="7191" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像組的每個頭像樣式 expect( avatar .getAttribute('style') ?.includes(`background: ${avatarStyleList[index].background}`) && avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content, ).toBe(true); if (index === 0) { //測試頭像形狀 expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true); } }); }); it('test avatar click callback correctly', () =" data-textnode-index-1701226829723="730" data-index-1701226829723="7653" data-index-len-1701226829723="7653" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //頭像點擊交互測試 const mockFn = jest.fn(); const component = mount( <Avatar size={54} triggerType="mask" triggerIcon={<CameraOutlined style={{ fontSize: '20px' }} /" data-textnode-index-1701226829723="740" data-index-1701226829723="7850" data-index-len-1701226829723="7850" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>} triggerClick={mockFn} " data-textnode-index-1701226829723="742" data-index-1701226829723="7887" data-index-len-1701226829723="7887" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <img src="https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png"" data-textnode-index-1701226829723="743" data-index-1701226829723="8006" data-index-len-1701226829723="8006" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="743" data-index-1701226829723="8012" data-index-len-1701226829723="8012" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> </Avatar" data-textnode-index-1701226829723="744" data-index-1701226829723="8027" data-index-len-1701226829723="8027" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>, ); act(() =" data-textnode-index-1701226829723="746" data-index-1701226829723="8047" data-index-len-1701226829723="8047" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { component.simulate('click'); }); let mockFnCallLength = mockFn.mock.calls.length; expect(mockFnCallLength).toBe(0); act(() =" data-textnode-index-1701226829723="753" data-index-1701226829723="8192" data-index-len-1701226829723="8192" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { component.setProps({ triggerType: 'button', }); }); component.update(); mockFnCallLength = mockFn.mock.calls.length; expect(mockFnCallLength).toBe(0); });});
拆解一下組件的源碼,測試最初的操作如下:
import React, { ReactNode } from 'react';import ReactDOM from 'react-dom';import Avatar from '../../Avatar/index';import AvatarGroup from '../../Avatar/group';import { CameraOutlined } from '@ant-design/icons';import Enzyme from '../setup';import mountTest from '../mountTest';import { act } from 'react-dom/test-utils';const { mount } = Enzyme;let container: HTMLDivElement | null;mountTest(Avatar);
和Button的測試區別點其實就在,定義了container容器,用于接下來的DOM測試。
//測試前準備容器 beforeEach(() =" data-textnode-index-1701226829723="825" data-index-1701226829723="8874" data-index-len-1701226829723="8874" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { container = document.createElement('div'); document.body.appendChild(container); }); //測試后刪除容器 afterEach(() =" data-textnode-index-1701226829723="840" data-index-1701226829723="8996" data-index-len-1701226829723="8996" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { document.body.removeChild(container as HTMLDivElement); container = null; });
在進行測試用例之前,創建了一個空div作為React測試的容器,放置React組件,并在測試用例結束后對該容器進行清除。
接下來我們開始分析測試用例:
第一組測試用例測試了文本頭像和圖片頭像的顯示正確性,首先給組件傳遞了一個test文本值,對文本值進行判斷。之后又給組件傳遞了一張圖片(ReactNode),并對組件中的圖片進行查詢判斷。
it('test avatar children content show correctly', () =" data-textnode-index-1701226829723="858" data-index-1701226829723="9305" data-index-len-1701226829723="9305" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像文本顯示 let contextText: string | ReactNode = 'test'; const component = mount(<Avatar" data-textnode-index-1701226829723="871" data-index-1701226829723="9406" data-index-len-1701226829723="9406" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>{contextText}</Avatar" data-textnode-index-1701226829723="872" data-index-1701226829723="9428" data-index-len-1701226829723="9428" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>); expect(component.find('.text-ref').text()).toEqual('test'); const imgSrc = 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png'; act(() =" data-textnode-index-1701226829723="888" data-index-1701226829723="9632" data-index-len-1701226829723="9632" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { contextText = <img src={imgSrc}" data-textnode-index-1701226829723="889" data-index-1701226829723="9672" data-index-len-1701226829723="9672" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="889" data-index-1701226829723="9678" data-index-len-1701226829723="9678" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>; }); expect(component.find('img')).toBeDefined();});
第二組測試用例較為復雜,沒有通過jest的渲染方式渲染組件,而是用上了之前所講到的container容器,并且創建了一個React虛擬DOM,渲染在測試用例環境中。這樣做其實也是因為測試用例本身是需要測試不同情況下的頭像樣式是否生效,因此會用到這種渲染方式。
it('test avatar group correctly', () =" data-textnode-index-1701226829723="900" data-index-1701226829723="9905" data-index-len-1701226829723="9905" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像樣式 const component = ( <AvatarGroup size={50} groupStyle={{ margin: '0 10px' }}" data-textnode-index-1701226829723="911" data-index-1701226829723="10005" data-index-len-1701226829723="10005" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <Avatar style={{ background: 'rgb(20, 169, 248)' }} shape="square"" data-textnode-index-1701226829723="916" data-index-1701226829723="10080" data-index-len-1701226829723="10080" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> View </Avatar" data-textnode-index-1701226829723="919" data-index-1701226829723="10111" data-index-len-1701226829723="10111" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <Avatar style={{ background: 'rgb(51, 112, 255)' }}" data-textnode-index-1701226829723="920" data-index-1701226829723="10171" data-index-len-1701226829723="10171" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>React</Avatar" data-textnode-index-1701226829723="921" data-index-1701226829723="10185" data-index-len-1701226829723="10185" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <Avatar style={{ background: 'rgb(0, 208, 184)' }}" data-textnode-index-1701226829723="924" data-index-1701226829723="10244" data-index-len-1701226829723="10244" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>UI</Avatar" data-textnode-index-1701226829723="925" data-index-1701226829723="10255" data-index-len-1701226829723="10255" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> </AvatarGroup" data-textnode-index-1701226829723="927" data-index-1701226829723="10275" data-index-len-1701226829723="10275" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> ); act(() =" data-textnode-index-1701226829723="931" data-index-1701226829723="10294" data-index-len-1701226829723="10294" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { ReactDOM.render(component, container); }); const avatarStyleList = [ { background: 'rgb(20, 169, 248)', content: 'View', }, { background: 'rgb(51, 112, 255)', content: 'React', }, { background: 'rgb(0, 208, 184)', content: 'UI', }, ]; const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement; expect(groupDom.childElementCount).toBe(3); const avatars = Array.from((container as HTMLDivElement).querySelectorAll('.avatar')); avatars.forEach((avatar, index) =" data-textnode-index-1701226829723="987" data-index-1701226829723="10889" data-index-len-1701226829723="10889" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像組的每個頭像樣式 expect( avatar .getAttribute('style') ?.includes(`background: ${avatarStyleList[index].background}`) && avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content, ).toBe(true); if (index === 0) { //測試頭像形狀 expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true); } }); });
通過ReactDOM.render渲染后,首先獲取了所有頭像的最外層容器:groupDom,并對頭像組所包含的頭像元素長度進行判斷,我這里是傳了三個頭像,因此預期應該為3。
const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement;expect(groupDom.childElementCount).toBe(3);
接下來獲取了所有頭像的DOM,并進行遍歷判斷,判斷自定義的頭像背景顏色和所傳文本內容是否相同,兩者都滿足,則該頭像的測試通過;并在我對第一個頭像設置了shape: square,這代表了這是一個方形頭像,因此在遍歷中需要對第一個頭像單獨做一次測試,判斷它的樣式是否生效(圓角)
avatars.forEach((avatar, index) =" data-textnode-index-1701226829723="1042" data-index-1701226829723="11695" data-index-len-1701226829723="11695" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //測試頭像組的每個頭像樣式 expect( avatar .getAttribute('style') ?.includes(`background: ${avatarStyleList[index].background}`) && avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content, ).toBe(true); if (index === 0) { //測試頭像形狀 expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true); } });
如上就是第二組測試用例,和之前測試用例不同的無非就是渲染方式和組件的樣式判斷,使用了原生的一些判斷,最后通過jest的toBe方法進行斷言。
第三組測試用例是交互測試,在對頭像設置了triggerIcon、triggerType、triggerClick后可變成交互頭像,具體顯示可查看組件庫文檔-Avatar頭像。這里也是先定義了一個mock函數,傳遞給組件作為回調函數測試,并且整體測試了mask、button兩種交互頭像的回調正確性
it('test avatar click callback correctly', () =" data-textnode-index-1701226829723="1088" data-index-1701226829723="12368" data-index-len-1701226829723="12368" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { //頭像點擊交互測試 const mockFn = jest.fn(); const component = mount( <Avatar size={54} triggerType="mask" triggerIcon={<CameraOutlined style={{ fontSize: '20px' }} /" data-textnode-index-1701226829723="1106" data-index-1701226829723="12565" data-index-len-1701226829723="12565" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>} triggerClick={mockFn} " data-textnode-index-1701226829723="1108" data-index-1701226829723="12602" data-index-len-1701226829723="12602" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> <img src="https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png"" data-textnode-index-1701226829723="1111" data-index-1701226829723="12721" data-index-len-1701226829723="12721" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">></img" data-textnode-index-1701226829723="1112" data-index-1701226829723="12727" data-index-len-1701226829723="12727" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> </Avatar" data-textnode-index-1701226829723="1114" data-index-1701226829723="12742" data-index-len-1701226829723="12742" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">>, ); act(() =" data-textnode-index-1701226829723="1118" data-index-1701226829723="12762" data-index-len-1701226829723="12762" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { component.simulate('click'); }); let mockFnCallLength = mockFn.mock.calls.length; expect(mockFnCallLength).toBe(0); act(() =" data-textnode-index-1701226829723="1132" data-index-1701226829723="12907" data-index-len-1701226829723="12907" class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; display: inline-block; text-indent: initial;">> { component.setProps({ triggerType: 'button', }); }); component.update(); mockFnCallLength = mockFn.mock.calls.length; expect(mockFnCallLength).toBe(0); });
如上就是頭像組件的所有測試用例。
測試React組件無非就是測試其交互性和樣式渲染正確性,因此筆者在React組件測試中使用最頻繁的就是文中所述的兩種渲染形式
因此掌握了這兩種渲染形式去書寫測試用例,可以測試到大部分的組件業務場景,在組件上線之前mock出更多的場景來避免錯誤發生。
本文鏈接:http://www.www897cc.com/showinfo-26-35305-0.html全網最細:Jest+Enzyme測試React組件(包含交互、DOM、樣式測試)
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com