自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

全網(wǎng)最細(xì):Jest+Enzyme測(cè)試React組件(包含交互、DOM、樣式測(cè)試)

開發(fā) 前端
在為組件添加prop傳值之前,可配置一個(gè)基礎(chǔ)的 mountTest.tsx 來對(duì)組件進(jìn)行一個(gè)基礎(chǔ)渲染掛載測(cè)試,測(cè)試通過后在進(jìn)行復(fù)雜情況下的測(cè)試。

介紹

Jest是目前前端工程化下單元測(cè)試火熱的技術(shù)棧,而Enzyme的支持提供了Jest測(cè)試React業(yè)務(wù)、組件的能力,下面來介紹一下React組件測(cè)試的一些實(shí)際場(chǎng)景。

1. 測(cè)試依賴包

"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",

2. 測(cè)試環(huán)境搭建

由于enzyme的配置在每次需要測(cè)試組件時(shí)都需要加入,因此配置setup.js后在每次測(cè)試組件中提前引入是不錯(cuò)的選擇。

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'],
};

3. 組件基礎(chǔ)渲染測(cè)試

在為組件添加prop傳值之前,可配置一個(gè)基礎(chǔ)的 mountTest.tsx 來對(duì)組件進(jìn)行一個(gè)基礎(chǔ)渲染掛載測(cè)試,測(cè)試通過后在進(jìn)行復(fù)雜情況下的測(cè)試。

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();
    });
  });
}

4. 組件交互相關(guān)測(cè)試

Button按鈕組件測(cè)試

這里拿Button按鈕舉例,具體Button組件可在http://react-view-ui.com:92/#/common/button參考,底部描述了組件的API能力。

圖片圖片

先看一下Button組件的整體測(cè)試文件,我一共分成了4組測(cè)試用例(不包含mountTest基礎(chǔ)測(cè)試)。

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;">> {
    //按鈕文字內(nèi)容測(cè)試
    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;">> {
    //按鈕點(diǎn)擊回調(diào)測(cè)試
    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;">> {
      //測(cè)禁用按鈕
      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;">> {
    //測(cè)試按鈕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;">> {
    //測(cè)試加載按鈕顯示
    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);

主要功能:引入必要的包、引入測(cè)試組件、引入組件渲染方式,這是是shallow和mount兩種,并在最后優(yōu)先進(jìn)行了組件基礎(chǔ)渲染測(cè)試。

第一組測(cè)試用例測(cè)試了Button按鈕的文字顯示正確性,是通過jest的find方法查詢到Button按鈕的DOM元素進(jìn)行判斷;之后設(shè)置了組件的disabled屬性,再次進(jìn)行點(diǎn)擊測(cè)試

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;">> {
    //按鈕文字內(nèi)容測(cè)試
    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');
});

第二組測(cè)試用例測(cè)試了按鈕的交互,在渲染組件之后,捕捉到按鈕的DOM,并自定義了mockFn函數(shù)傳遞給實(shí)際Button組件后進(jìn)行回調(diào)測(cè)試,Button我在點(diǎn)擊時(shí)是沒有傳參的,因此回調(diào)參數(shù)長度為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;">> {
    //按鈕點(diǎn)擊回調(diào)測(cè)試
    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;">> {
      //測(cè)禁用按鈕
      component.setProps({
        disabled: true,
      });
    });

    button.simulate('click');
    expect(mockFn.mock.calls.length).toBe(mockFnCallLength);
});

第三組測(cè)試用例對(duì)Button按鈕類型進(jìn)行了測(cè)試,傳遞了type:primary,并對(duì)渲染后的組件進(jìn)行判斷是否有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;">> {
    //測(cè)試按鈕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);
});

第四組測(cè)試用例對(duì)loading Button進(jìn)行了測(cè)試,同樣也是檢查類名的形式,與第三組測(cè)試用例類似

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;">> {
    //測(cè)試加載按鈕顯示
    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();
  });

這就是我對(duì)Button的測(cè)試。

Avatar頭像組件測(cè)試

由于Button組件本身功能比較簡(jiǎn)單,可擴(kuò)展性有限,作為第一個(gè)組件案例進(jìn)行舉例。

接下來對(duì)Avatar組件進(jìn)行測(cè)試。

圖片圖片

還是先上測(cè)試源碼。

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;">> {
  //測(cè)試前準(zhǔn)備容器
  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);
  });
  //測(cè)試后刪除容器
  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;">> {
    //測(cè)試頭像文本顯示
    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;">> {
    //測(cè)試頭像樣式
    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;">> {
      //測(cè)試頭像組的每個(gè)頭像樣式
      expect(
        avatar
          .getAttribute('style')
          ?.includes(`background: ${avatarStyleList[index].background}`) &&
          avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content,
      ).toBe(true);
      if (index === 0) {
        //測(cè)試頭像形狀
        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;">> {
    //頭像點(diǎn)擊交互測(cè)試
    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);
  });
});

拆解一下組件的源碼,測(cè)試最初的操作如下:

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的測(cè)試區(qū)別點(diǎn)其實(shí)就在,定義了container容器,用于接下來的DOM測(cè)試。

//測(cè)試前準(zhǔn)備容器
  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);
  });
  //測(cè)試后刪除容器
  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;
  });

在進(jìn)行測(cè)試用例之前,創(chuàng)建了一個(gè)空div作為React測(cè)試的容器,放置React組件,并在測(cè)試用例結(jié)束后對(duì)該容器進(jìn)行清除。

接下來我們開始分析測(cè)試用例:

第一組測(cè)試用例測(cè)試了文本頭像和圖片頭像的顯示正確性,首先給組件傳遞了一個(gè)test文本值,對(duì)文本值進(jìn)行判斷。之后又給組件傳遞了一張圖片(ReactNode),并對(duì)組件中的圖片進(jìn)行查詢判斷。

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;">> {
    //測(cè)試頭像文本顯示
    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();
});

第二組測(cè)試用例較為復(fù)雜,沒有通過jest的渲染方式渲染組件,而是用上了之前所講到的container容器,并且創(chuàng)建了一個(gè)React虛擬DOM,渲染在測(cè)試用例環(huán)境中。這樣做其實(shí)也是因?yàn)闇y(cè)試用例本身是需要測(cè)試不同情況下的頭像樣式是否生效,因此會(huì)用到這種渲染方式。

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;">> {
    //測(cè)試頭像樣式
    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;">> {
      //測(cè)試頭像組的每個(gè)頭像樣式
      expect(
        avatar
          .getAttribute('style')
          ?.includes(`background: ${avatarStyleList[index].background}`) &&
          avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content,
      ).toBe(true);
      if (index === 0) {
        //測(cè)試頭像形狀
        expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true);
      }
    });
  });

通過ReactDOM.render渲染后,首先獲取了所有頭像的最外層容器:groupDom,并對(duì)頭像組所包含的頭像元素長度進(jìn)行判斷,我這里是傳了三個(gè)頭像,因此預(yù)期應(yīng)該為3。

const groupDom = (container as HTMLDivElement).querySelector('.avatar-group') as HTMLElement;
expect(groupDom.childElementCount).toBe(3);

接下來獲取了所有頭像的DOM,并進(jìn)行遍歷判斷,判斷自定義的頭像背景顏色和所傳文本內(nèi)容是否相同,兩者都滿足,則該頭像的測(cè)試通過;并在我對(duì)第一個(gè)頭像設(shè)置了shape: square,這代表了這是一個(gè)方形頭像,因此在遍歷中需要對(duì)第一個(gè)頭像單獨(dú)做一次測(cè)試,判斷它的樣式是否生效(圓角)

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;">> {
      //測(cè)試頭像組的每個(gè)頭像樣式
      expect(
        avatar
          .getAttribute('style')
          ?.includes(`background: ${avatarStyleList[index].background}`) &&
          avatar.querySelector('.text-ref')?.innerHTML === avatarStyleList[index].content,
      ).toBe(true);
      if (index === 0) {
        //測(cè)試頭像形狀
        expect(avatar.getAttribute('style')?.includes(`border-radius: 5px`)).toBe(true);
      }
    });

如上就是第二組測(cè)試用例,和之前測(cè)試用例不同的無非就是渲染方式和組件的樣式判斷,使用了原生的一些判斷,最后通過jest的toBe方法進(jìn)行斷言。

第三組測(cè)試用例是交互測(cè)試,在對(duì)頭像設(shè)置了triggerIcon、triggerType、triggerClick后可變成交互頭像,具體顯示可查看組件庫文檔-Avatar頭像。這里也是先定義了一個(gè)mock函數(shù),傳遞給組件作為回調(diào)函數(shù)測(cè)試,并且整體測(cè)試了mask、button兩種交互頭像的回調(diào)正確性

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;">> {
    //頭像點(diǎn)擊交互測(cè)試
    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);
  });

如上就是頭像組件的所有測(cè)試用例。

小結(jié)

測(cè)試React組件無非就是測(cè)試其交互性和樣式渲染正確性,因此筆者在React組件測(cè)試中使用最頻繁的就是文中所述的兩種渲染形式

  • Jest渲染(mount、render、shallow)
  • ReactDOM渲染(用于測(cè)試樣式、元素節(jié)點(diǎn))

因此掌握了這兩種渲染形式去書寫測(cè)試用例,可以測(cè)試到大部分的組件業(yè)務(wù)場(chǎng)景,在組件上線之前mock出更多的場(chǎng)景來避免錯(cuò)誤發(fā)生。

責(zé)任編輯:武曉燕 來源: 量子前端
相關(guān)推薦

2017-03-21 21:37:06

組件UI測(cè)試架構(gòu)

2022-10-26 08:00:49

單元測(cè)試React

2020-03-19 14:50:31

Reac單元測(cè)試前端

2021-06-26 07:40:21

前端自動(dòng)化測(cè)試Jest

2024-05-09 16:21:46

Deepseek技術(shù)算法

2023-05-18 14:01:00

前端自動(dòng)化測(cè)試

2021-10-12 19:16:26

Jest單元測(cè)試

2022-02-04 22:18:28

React路由應(yīng)用

2025-04-18 16:05:39

2021-02-26 15:10:00

前端React組件交互

2012-09-03 13:51:43

測(cè)試軟件測(cè)試單元測(cè)試

2022-07-01 08:00:00

自動(dòng)處理Mockoon測(cè)試

2023-11-08 13:18:00

JestJavaScript框架

2022-08-09 13:08:27

VitestJest前端

2013-04-08 09:28:09

測(cè)試

2022-04-14 08:00:00

Cypress測(cè)試開發(fā)

2017-01-01 09:43:40

2010-01-20 16:13:15

2023-04-09 15:08:20

Cypress組件測(cè)試

2012-04-09 10:07:08

JavaScript
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)