前端单元测试技术方案总结(下)

发布于 2021-3-9 09:37
浏览
0收藏

JEST

 

前端单元测试技术方案总结(下)-开源基础软件社区

Jest 是 facebook 出的一个完整的单元测试技术方案,集 测试框架, 断言库, 启动器, 快照,沙箱,mock工具于一身,也是 React 官方使用的测试工具。Jest 和 Jasmine 具有非常相似的 API ,所以在 Jasmine 中用到的工具在 Jest 中依然可以很自然地使用。可以近似看作 Jest = JSDOM 启动器 + Jasmine 。

 

虽然 Jest 提供了很丰富的功能,但是并没有内置 ES6 支持,所以依然需要根据不同运行时对代码进行转换,由于 Jest 主要运行在 Node 中,所以需要使用 babel-jest 将 ES Module 转换成 CommonJS 。

 

Jest 的默认配置

npm install jest --save-dev
npx jest --init
√ Would you like to use Jest when running "test" script in "package.json"? ... yes
√ Would you like to use Typescript for the configuration file? ... no
√ Choose the test environment that will be used for testing » jsdom (browser-like)
√ Do you want Jest to add coverage reports? ... no
√ Which provider should be used to instrument code for coverage? » babel
√ Automatically clear mock calls and instances between every test? ... yes

 

在 Node 或 JSDOM 下增加 ES6代码的支持

npm install jest-babel @babel/core @babel/preset-env
// .babelrc
{
    "presets": ["@babel/preset-env"]
}
// jest.config.js
// 下面两行为默认配置,不写也可以
{
+    testEnvironment: "jsdom",
+    transform: {"\\.[jt]sx?$": "babel-jest"}
}

 

使用 Jest 生成测试报告前端单元测试技术方案总结(下)-开源基础软件社区对于 React 和 TypeScript 支持也可以通过修改 babel 的配置解决

npm install @babel/preset-react @babel/preset-typescript --save-dev
// .babrlrc
{
    "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}

 

Jest 在真实浏览器环境下测试


目前 Jest 不支持直接在真实浏览器中进行测试,其默认的启动器只提供了一个 JSDOM 环境,在浏览器中进行单元测试目前只有 Karma 方案能做到,所以也可以使用 Karma + Jest 方案实现,但是不建议这么做,因为 Jest 自身太重,使用 Karma + Jasmine 能达到基本一样的效果。

另外还有一个比较流行的 E2E 方案 Jest + Puppeteer , 由于 E2E 不属于单元测试范畴,这里不再展开。

 

Jest 工具链总结

 

  • Node 环境下测试 : Jest + babel
  • JSDOM 测试 : Jest + babel
  • 真实浏览器测试(不推荐)
  • E2E 测试 : Jest + Puppeteer


稍作总结


上面的内容介绍了 chai , mocha , karma , jasmine 和 jest, 每种工具分别对应一些自己特有的工具链,在选取合适的测试工具时根据实际需要选择, 测试领域还有非常多的工具数都数不过来,下面来看下 React 单元测试的一些方法。

 

使用 Jest + Enzyme 对 React 进行单元测试

 

前端单元测试技术方案总结(下)-开源基础软件社区Enzyme基础配置如下:

npm install enzyme enzyme-adapter-react-16 jest-enzyme jest-environment-enzyme jest-canvas-mock react@16 react-dom@16 --save-dev
// jest.config.js
{
- "testEnvironment": "jsdom",
+  setupFilesAfterEnv: ["jest-enzyme", "jest-canvas-mock"],
+  testEnvironment: "enzyme",
+  testEnvironmentOptions: {
+    "enzymeAdapter": "react16"
+  },
}

 

jest-canvas-mock 这个包是为了解决一些使用 JSDOM 未实现行为触发警告的问题。

上面建立了一个使用 Enzyme 比较友好的环境,可以直接在全局作用域里引用 React , shallow, mount 等 API。此外 Enzyme 还注册了许多友好的断言函数到 Jest 中,如下所示,参考地址

toBeChecked()
toBeDisabled()
toBeEmptyRender()
toExist()
toContainMatchingElement()
toContainMatchingElements()
toContainExactlyOneMatchingElement()
toContainReact()
toHaveClassName()
toHaveDisplayName()
toHaveHTML()
toHaveProp()
toHaveRef()
toHaveState()
toHaveStyle()
toHaveTagName()
toHaveText()
toIncludeText()
toHaveValue()
toMatchElement()
toMatchSelector()
// js/ClassComponent.js
import React from 'react';

export default class ClassComponent extends React.PureComponent {
    constructor() {
        super();
        this.state = { name: 'classcomponent' };
    }
    render() {
        return (
            <div>
                a simple class component
                <CustomComponent />
            </div>
        );
    }
}

// test/hook.test.js
import HookComponent from '../js/HookComponent';

describe('HookComponent', () => {
    it ('test with shallow', () => {
        const wrapper = shallow(<HookComponent id={1} />);
        expect(wrapper).toHaveState('name', 'classcomponent');
        expect(wrapper).toIncludeText('a simple class component');
        expect(wrapper).toContainReact(<div>a simple class component</div>);
        expect(wrapper).toContainMatchingElement('CustomComponent');
    })
})

 

Enzyme 提供了三种渲染组件方法

 

  • shallow 使用 react-test-renderer 将组件渲染成内存中的对象, 可以方便进行 props, state 等数据方面的测试,对应的操作对象为 ShallowWrapper,在这种模式下仅能感知到第一层自定义子组件,对于自定义子组件内部结构则无法感知。
  • mount 使用 react-dom 渲染组件,会创建真实 DOM 节点,比 shallow 相比增加了可以使用原生 API 操作 DOM 的能力,对应的操作对象为 ReactWrapper ,这种模式下感知到的是一个完整的 DOM 树。
  • render 使用 react-dom-server 渲染成 html 字符串,基于这份静态文档进行操作,对应的操作对象为 CheerioWrapper。


Shallow 渲染


因为 shallow 模式仅能感知到第一层自定义子组件组件,往往只能用于简单组件测试。例如下面的组件

// js/avatar.js
function Image({ src }) {
    return ![]({src} />);
}

function Living({ children }) {
    return <div className="icon-living"> { children } </div>;
}

function Avatar({ user, onClick }) {
    const { living, avatarUrl } = user;
    return (
        <div className="container" onClick={onClick}>
            <div className="wrapper">
              <Living >
                <div className="text"> 直播中 </div>
              </Living>
            </div>
            <Image src={avatarUrl} />
        </div>
    )
}

export default Avatar;

 

shallow 渲染虽然不是真正的渲染,但是其组件生命周期会完整地走一遍。

使用 shallow(<Avatar />) 能感知到的结构如下, 注意看到 div.text 作为 Living 组件的 children 能够被检测到,但是 Living 的内部结构无法感知。前端单元测试技术方案总结(下)-开源基础软件社区

Enzyme 支持的选择器支持我们熟悉的 css selector 语法,这种情况下我们可以对 DOM 结构做如下测试

// test/avatar.test.js
import Avatar from '../js/avatar';

describe('Avatar', () => {
    let wrapper = null, avatarUrl = 'abc';

    beforeEach(() => {
        wrapper = shallow(<Avatar user={{ avatarUrl: avatarUrl }} />);
    })

    afterEach(() => {
        wrapper.unmount();
        jest.clearAllMocks();
    })

    it ('should render success', () => {
        // wrapper 渲染不为空
        expect(wrapper).not.toBeEmptyRender();
        // Image 组件渲染不为空, 这里会执行 Image 组件的渲染函数
        expect(wrapper.find('Image')).not.toBeEmptyRender();
        // 包含一个节点
        expect(wrapper).toContainMatchingElement('div.container');
        // 包含一个自定义组件
        expect(wrapper).toContainMatchingElement("Image");
        expect(wrapper).toContainMatchingElement('Living');
        // shallow 渲染不包含子组件的内部结构
        expect(wrapper).not.toContainMatchingElement('img');
        // shallow 渲染包含 children 节点
        expect(wrapper).toContainMatchingElement('div.text');
        // shallow 渲染可以对 children 节点内部结构做测试
        expect(wrapper.find('div.text')).toIncludeText('直播中');
    })
})

 

如果我们想去测试对应组件的 props / state 也可以很方便测试,不过目前存在缺陷,Class Component 能通过 toHaveProp, toHaveState 直接测试, 但是 Hook 组件无法测试 useState 。

it ('Image component receive props', () => {
  const imageWrapper = wrapper.find('Image');、
  // 对于 Hook 组件目前我们只能测试 props
  expect(imageWrapper).toHaveProp('src', avatarUrl);
})

 

wrapper.find 虽然会返回同样的一个 ShallowWrapper 对象,但是这个对象的子结构是未展开的,如果想测试imageWrapper 内部结构,需要再 shallow render 一次。

it ('Image momponent receive props', () => {
  const imageWrapper = wrapper.find('Image').shallow();

  expect(imageWrapper).toHaveProp('src', avatarUrl);
  expect(imageWrapper).toContainMatchingElement('img');
  expect(imageWrapper.find('img')).toHaveProp('src', avatarUrl);
})

 

也可以改变组件的 props, 触发组件重绘

it ('should rerender when user change', () => {
    const newAvatarUrl = '' + Math.random();
    wrapper.setProps({ user: { avatarUrl: newAvatarUrl }});
    wrapper.update();
    expect(wrapper.find('Image')).toHaveProp('src', newAvatarUrl);
})

 

另一个常见的场景是事件模拟,事件比较接近真实测试场景,这种场景下使用 shallow 存在诸多缺陷,因为 shallow 场景事件不会像真实事件一样有捕获和冒泡流程,所以此时只能简单的触发对应的 callback 达到测试目的。

 

it ('will call onClick prop when click event fired', () => {
    const fn = jest.fn();

    wrapper.setProps({ onClick: fn });
    wrapper.update();

    // 这里触发了两次点击事件,但是 onClick 只会被调用一次。
    wrapper.find('div.container').simulate('click');
    wrapper.find('div.wrapper').simulate('click');
    expect(fn).toHaveBeenCalledTimes(1);
})

 

关于这些网上有人总结了 shallow 模式下的一些不足

  1. shallow 渲染不会进行事件冒泡,而 mount 会。
  2. shallow 渲染因为不会创建真实 DOM,所以组件中使用 refs 的地方都无法正常获取,如果确实需要使用 refs , 则必须使用 mount。
  3. simulate 在 mount 中会更加有用,因为它会进行事件冒泡。


其实上面几点说明了一个现象是 shallow 往往只适合一种理想的场景,一些依赖浏览器行为表现的操作 shallow 无法满足,这些和真实环境相关的就只能使用mount了。

 

Mount 渲染
Mount 渲染的对象结构为 ReactWrapper 其提供了和 ShallowWrapper 几乎一样的 API , 差异很小。

在 API层面的一些差异如下

+ getDOMNode()        获取DOM节点
+ detach()            卸载React组件,相当于 unmountComponentAtNode
+ mount()             挂载组件,unmount之后通过这个方法重新挂载
+ ref(refName)        获取 class component 的 instance.refs 上的属性
+ setProps(nextProps, callback)
- setProps(nextProps)
- shallow()
- dive()
- getElement()
- getElements()

另外由于 mount 使用 ReactDOM 进行渲染,所以其更加接近真实场景,在这种模式下我们能观察到整个 DOM 结构和React组件节点结构。

前端单元测试技术方案总结(下)-开源基础软件社区

describe('Mount Avatar', () => {
    let wrapper = null, avatarUrl = '123';

    beforeEach(() => {
        wrapper = mount(<Avatar user={{ avatarUrl }} />);
    })

    afterEach(() => {
        jest.clearAllMocks();
    })

    it ('should set img src with avatarurl', () => {
        expect(wrapper.find('Image')).toExist();
        expect(wrapper.find('Image')).toHaveProp('src', avatarUrl);
        expect(wrapper.find('img')).toHaveProp('src', avatarUrl);
    })
})

 

在 shallow 中无法模拟的事件触发问题在 mount 下就不再是问题。

it ('will call onClick prop when click event fired', () => {
    const fn = jest.fn();

    wrapper.setProps({ onClick: fn });
    wrapper.update();

    wrapper.find('div.container').simulate('click');
    wrapper.find('div.wrapper').simulate('click');
    expect(fn).toHaveBeenCalledTimes(2);
})

总结一下 shallow 中能做的 mount 都能做,mount中能做的 shallow 不一定能做。

 

Render 渲染


render 内部使用 react-dom-server 渲染成字符串,再经过 Cherrio 转换成内存中的结构,返回 CheerioWrapper 实例,能够完整地渲染整个DOM 树,但是会将内部实例的状态丢失,所以也称为 Static Rendering 。这种渲染能够进行的操作比较少,这里也不作具体介绍,可以参考 官方文档 。

 

总结


如果让我推荐的话,对于真实浏览器我会推荐 Karma + Jasmine 方案测试,对于 React 测试 Jest + Enzyme 在 JSDOM 环境下已经能覆盖大部分场景。另外测试 React组件除了 Enzyme 提供的操作, Jest 中还有很多其他有用的特性,比如可以 mock 一个 npm 组件的实现,调整 setTimeout 时钟等,真正进行单元测试时,这些工具也是必不可少的,整个单元测试技术体系包含了很多东西,本文无法面面俱到,只介绍了一些距离我们最近的相关的技术体系。

标签
已于2021-3-9 09:37:00修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐