组件的单元测试

335 阅读4分钟

因为是从0 到 1 进行搭建组件,首先先创建一个next 的项目。本文只用于记录自动化测试

技术选型为:Jest + Enzyme

测试工程搭建

  1. 先创建一个react app
npx create-react-app test-with-cra
  1. 添加enzyme 等包
yarn add enzyme enzyme-adapter-react-16 -D
  1. 进行相关配置,这里暂时没有生效
  • 创建 src/setupTest.js文件

image.png

  • 文件内容
// src/setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

现在就可以执行yarn test 进行测试了。

image.png

测试 React 组件

我们先写一个简单的js的例子,然后让这个例子运行起来,目录结构如上: image.png

// Button/index.jsx
import React, { useEffect, useState } from "react";
import services from "./services";
import tracker from "./tracker";

const Button = (props) => {
  const [buttonName, setBtnName] = useState("buttonDefaultName");
  const [data, setData] = useState({});

  const getData = async () => {
    const res = await services.fetchData();
    setData(res);
  };
  tracker.page("button init");

  useEffect(()=>{
    getData();
  },[])

  useEffect(() => {
    setTimeout(() => {
      setBtnName("buttonAfter3seconds");
    }, 3000);
  }, []);

  const clickBtn = () => {
    console.log(data);
    props.onButtonClick && props.onButtonClick();
  };

  return (
    <div>
      <div className="button" onClick={clickBtn}>{props.buttonName  || buttonName}</div>
    </div>
  );
};

export default Button;
// src/Button/services.js
const fetchData = async ()=>{
  // 一个请求,先随便写一个吧
 return "mockdata"
}

const services={
  fetchData
}

export default services;

// src/Button/tracker.js
const page = (value) => {
  // 是一个函数,假设这里是一个埋点函数
  console.log(value);
};

const tracker={
  page
}

export default tracker;

然后修改一下App.js

import MyButton from'./Button/index';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <MyButton></MyButton>
      </header>
    </div>
  );
}

export default App;

启动一下,跑起来的页面是这样的:

image.png

我们的测试目的会围绕这几个关键点:

  1. 组件是否符合预期的渲染了?
  2. 事件点击的回调函数是否正确执行了?
  3. 业务埋点函数是否被正常调用了?
  4. 异步接口请求如何校验?
  5. setimeout 等异步后的操作如何校验?

组件是否符合预期的渲染了

当传入属性不同的时候,可能会导致UI的渲染也是不一样的。

propsA → Component Render → renderResultA
propsB → Component Render → renderResultB

让我们看一下

image.png

//__test__/button1.test.js
// snapshot

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react'
import { shallow } from 'enzyme';
configure({ adapter: new Adapter() });

describe('Button 组件测试', () => {
it('渲染正常', () => {
  const Button = require('./Button').default
  // enzyme的shallow方法用于渲染组件
  const componentWrapper = shallow(<Button />);
  expect(componentWrapper.html()).toMatchSnapshot();
});
it('props传入buttonName,渲染正常', () => {
  const Button = require('./Button').default
  const componentWrapper = shallow(<Button buttonName={'mockButtonName'}/>);
  expect(componentWrapper.html()).toMatchSnapshot();
});
});

让我们执行一个 yarn test 这个时候会生成一个文件快照生成不同条件下的结果:

image.png

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Button 组件测试 props传入buttonName,渲染正常 1`] = `"<div>mockButtonName</div>"`;

exports[`Button 组件测试 渲染正常 1`] = `"<div>buttonDefaultName</div>"`;

当我们下次改动到了组件的渲染逻辑的时候,snapshot会提醒渲染和之前是不一样的,要是在预期内的改动,可以更新改动,若不是预期内的改动,需要看一下问题出现在哪里。

image.png

事件点击的回调函数是否正确执行了

对于一些组件的行为,我们可以通过 Enzyme渲染出的组件进行模拟,并断言结果。

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react'
import { shallow } from 'enzyme';
configure({ adapter: new Adapter() });

describe('Button 组件测试', () => {
  it('click点击事件测试,mockfn校验', () => {
    // 生成一个mockfn,用于校验点击后的props.func是否被调用
    const mockClick = jest.fn()
    const Button = require('./Button').default
    const componentWrapper = shallow(<Button onButtonClick={mockClick}/>);
    // 模拟点击事件
    componentWrapper.find('.button').at(0).simulate('click')
    // 校验mockfn被调用
    expect(mockClick).toHaveBeenCalled();
  });
});

jest.fn() 用来生成mock函数,可以通过mockClick 这个句柄去判断函数的调用情况

enzyme提供了类似于jquerydom选择器,并通过 simulate(event) 的方式来模拟事件

业务埋点函数是否被正常调用了

我们可以看到组件通过模块的方式引入了一个 tracker 函数,用于在组件初始化的时候触发埋点行为,那如何验证tracker是否被正常调用呢?我们下面的写法就是用mock的函数替代原来的函数。

//__test__/button1.test.js
// snapshot
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
import { shallow } from "enzyme";
configure({ adapter: new Adapter() });

describe("Button 组件测试", () => {
  it("校验埋点是否被正常调用", () => {
    // 生成一个mock函数
    const page = jest.fn();
    // 声明 mock tracker这个模块,并在第二个参数传入mock方法
    jest.doMock("./Button/tracker", () => {
      return { page : page};
    });
    const Button = require("./Button").default;
    shallow(<Button />);
    // mock函数被调用,并且参数是 'button init'
    expect(page).toHaveBeenCalledWith("button init")
  });
});

异步接口请求

类组件的写法如下:

describe('Button 组件测试',  () => {
  it('接口测试',async () => {
    jest.doMock('./Button/services',()=>{
      return ()=>{
        return 'c'
      }
    })
    const Button = require('./Button').default
    const componentWrapper = await shallow(<Button />);
    // 通过enzyme渲染的组件可以通过 .state() 方法拿到组件 state 状态
    // expect(componentWrapper.state('data')).toEqual('mockdata')
    expect(componentWrapper.state("data")).toEqual('mockdata')
  });
});

hook 相关的写法参考 :github.yanhaixiang.com/jest-tutori…

setimeout 等异步操作是否按预期执行了

那么如何测试到三秒后的状态呢?在测试中等三秒吗?显然不是,我们可以使用jest 提供的faketimer 来帮我们快进时间

describe('Button 组件测试', () => {
  it('3秒后渲染正常', () => {
    const Button = require('./Button').default
    const componentWrapper = shallow(<Button />);
    // 快速执行所有的 macro-task (eg. setTimeout(), setInterval())
    jest.runAllTimers()
    expect(componentWrapper.html()).toMatchSnapshot();
  });
});

自测报告

image.png

  • Statements 语句覆盖率,它其实对应的就是js语法上的语句,js解析成ast数中类型为statement。
  • Branches 分支覆盖率,通俗点理解就是if/else这类条件
  • Functions 函数覆盖率
  • Lines 行数覆盖率,就是代码执行了多少行

所有断言的枚举:man.hubwiz.com/manual/Jest

文献参考:zhuanlan.zhihu.com/p/147804513