React--单元测试

Author Avatar
mxx 6月 01, 2017
  • 在其它设备中阅读本文章

单元测试的概念

单元测试 是针对 程序的最小单元 来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是单个程序、类、对象、方法等

一般对于复用性较高的组件需要进行单元测试

  • 测试元素
<div class='header'></div>

测试其class属性值是不是‘Header’

  • 测试函数
function test (arg) {
  return Boolean (arg)
}

测试其返回值是否为正确的布尔值

  • 测试行为
<button onclick='javascript:alert("hi, i am Sheldon")'> click me </button>

测试点击行为是否触发正确结果

单元测试常用工具

css

Karma是一个基于Node.js的JavaScript测试执行过程管理工具

karma会启动一个web服务器,将js源代码和测试脚本放到PhantomJS或者Chrome上执行

karma的配置文件 karma.conf.js 有以下属性配置

files: './test/**/*.spec.js',
preprocessors: {
  './test/**/*.spec.js': ['webpack']
}

这个是用来判断哪些文件需要进行测试 以及 覆盖率处理的

当项目中测试文件很多的时候 可以考虑更改此处参数 指定文件夹或者文件进行单元测试 从而节省测试时间

PhantomJS 是一个基于 WebKit 的服务器端 JavaScript API。它全面支持web而不需浏览器支持,其快速,原生支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。 PhantomJS 可以用于 页面自动化 , 网络监测 , 网页截屏 ,以及 无界面测试 等

可以将其理解为-个虚拟的具有webkit内核的浏览器环境

jasmine是一款测试框架,它不依赖于其他任何 JavaScript 组件

Jasmine 断言库 是单元测试的语句编译规则

Jasmine 语法规则

  • describe(string, function) 测试集,官方称之为suite
    主要功能是用来划分单元测试的,describe是可以嵌套使用的
参数 说明
string 描述测试集的信息
function 测试集的具体实现
  • it(string, function) 测试用例 官方称之为spec
参数 说明
string 描述测试用例的信息
function 测试用例的具体实现

简单的测试案例

describe('suite describe', function() {
  it('spec describe', function() {
    ......
  })
})
  • expect(arg) 断言
参数 说明
arg 要测试的实际值

断言语句,以expect语句表示,返回true或false

只有当一个测试用例中的所有的断言语句全为true时,这个Spec才通过,否则失败

常用的断言语句

expect(x).toEqual(y) //比较x和y是否相等
expect(x).not.toEqual(y) //比较x和y是否不相等
expect(x).toBe(y) //比较x和y是否是相同的对象
expect(x).toBeDefined() //x是否为undefined
expect(x).toBeNull() //x是否为null
expect(x).toBeTruthy() //x是否为true
expect(x).toBeFalsy() //x是否为false
expect(x).toContain(y) //x是否包含y (x可以是字符串或数组)

断言语句使用案例

expect(1).toBe(1)
expect('a').not.toBe('b')
expect(false).toEqual(false)

Airbnb开源的 React 测试类库

Enzyme 是非常友好的React测试工具 提供了类似于jQuery API的react节点选取方式

比如获取一个元素

  ...
  wrapper.find('.header')

Enzyme 对组件提供了三种渲染方式

1 shallow
2 mount
3 render

  • 1 shallow 浅渲染

把组件当做一个单元来测试,而且确保不会因为子组件的行为而直接出现断言

Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren’t indirectly asserting on behavior of child components

shallow方法只渲染出组件的第一层DOM结构,其嵌套的子组件不会被渲染出来,其渲染的效率更高,单元测试的速度更快

常用API

.at()
.exists()
.find()
.get()
.html()
.isEmpty()
.prop()
.props()
.text()
  • 2 mount

mount 方法则会将 React 组件渲染为真实的 DOM 节点

当测试用例需要依赖真实的 DOM 结构(比如说按钮的点击事件) 需要使用mount进行处理

使用案例

it('render header component', () => {
  const wrapper = mount(<Header {...headerProps} />)
  ...
})

常用API

.hasClass()
.mount()
.render()

项目中使用的时候 将mount方法进行了封装

export const mountWithRouter = (ReactNode, routerProps = {}, context = {}) => {
  return mount(
    <MemoryRouter {...routerProps}>
      {ReactNode}
    </MemoryRouter>
  , context)
}

借助react-router-dom中提供的MemoryRouter方法可以传入路由地址 从而进行涉及到路由变化的测试

比如测试当前哈希值为 ‘/intro’ 的时候页面是否包含指定组件

---simplify code---
const headerProps = {
  backBtnComponent: <BackBtnComponent />,
  introBtnComponent: <IntroComponent />
}

const wrapper = mountWithRouter(<Header {...headerProps} />, {
  initialEntries: ['/intro']
})
const backBtnComponentWrapper = wrapper.find('.header__btn')
expect(backBtnComponentWrapper.contains(<BackBtnComponent />)).toEqual(true)
---simplify code---
  • 3 render

render方法将React组件渲染成静态的HTML字符串,然后分析这段HTML代码的结构,返回一个对象

Enzyme’s render function is used to render react components to static HTML and analyze the resulting HTML structure.

使用案例

  it('renders three `.foo-bar`s', () => {
    const wrapper = render(<Foo />)
    expect(wrapper.find('.foo-bar')).to.have.length(3)
  })

这三种方法的API很多是相同的 完整的可查看Enzyme提供的API列表

simulate 方法

Enzyme的API中有一个simulate方法 它在组件上模拟触发某个DOM事件,比如 Click,Change 等等

.simulate() 方法将会根据模拟的事件触发这个组件的 prop

例如 .simulate(‘click’) 实际上会获取组件props中的onClick属性并进行调用

当需要检查一个组件中某个函数是否被调用时 需要经过三个步骤

1 使用 jasmine.createSpy() 方法监视所传入该组件作为 prop 的 spyOnClick 方法
2 通过simulate 方法模拟一个 Click 事件
3 编写断言判断函数是否被调用

const spyOnClick = jasmine.createSpy('introOnClick')
-- simplify code --

Wrapper.simulate('click') // 表示进行点击
expect(spyOnClick).toHaveBeenCalled() // 判断函数是否被调用

单元测试–代码覆盖率

单元测试中常用代码覆盖率来衡量有没有将组件测试完整

一般从三个方向来度量代码的覆盖程度

  • 1 语句覆盖

用来度量被测代码中每个可执行语句是否被执行到了

  • 2 判定覆盖

又称分支覆盖 用来度量程序中每一个判定的分支是否都被测试到了

  • 3 条件覆盖

用来度量判定中的每个子表达式结果true和false是否被测试到了

在Karma相关配置文件中添加属性

reporters: ['spec', 'coverage'],
coverageReporter: {
  dir: './coverage',
  reporters: [{ type: 'html' }]
}

会生成HTML文件 可以看到各个组件的测试覆盖率
coverage
每一个组件还有各自代码覆盖率详情 如果测试用例没有覆盖到某一个组件或者函数 也会有提示
coverage

React项目的单元测试的覆盖率有一个问题

因为项目中使用到webpack进行项目处理,所以测试的时候会包含webpack打包完以后的文件

代码覆盖率在JS代码不需要编译的情况下。直接可以使用karma提供的karma-coverage进行测试率统计

如果项目有使用打包编译工具,这个覆盖率会降低到10%-20%,失去参考价值

在GitHub中有项目使用 isparta、isparta-instrumenter-loader、istanbul等进行此方面的处理

单元测试实例展示

1 测试元素

React公共组件Header结构如下

export default () => {
  return (
    <div className='header'>
      <div className='header-button' />
    </div>
  )
}

单元测试要点
1 测试元素 .header 是否存在
2 测试元素 .header-button 是否存在

编写单元测试

import React from 'react'
import Header from '../components/Header/index.js'

describe('Header', () => {
  it(`
    should render header
  `, () => {
    const wrapper = shallow(<Header />)
    expect(wrapper.find('header.header').length).toEqual(1)
    expect(wrapper.find('.header-button').length).toEqual(1)
  })
})

2 测试点击行为

React公共组件Header结构如下

export default () => {
  return (
    <div className='header'>
        {introBtnComponent && (introOnClick
          ? <a
              className="header__btn"
              href="javascript:;"
              onClick={() => introOnClick()}
            >
              {introBtnComponent}
            </a>
          : <Link className="header__btn" to={INTRO_PATH}>
              {introBtnComponent}
            </Link>
        )}
    </div>
  )
}

单元测试要点
1 测试点击行为是否发生
2 测试组件introBtnComponent 是否存在

it(`
  should render introBtnComponent after introOnClick
`, () => {
  const IntroBtnComponent = () => <div>IntroBtnComponent</div>
  const spyOnClick = jasmine.createSpy('introOnClick')
  const headerProps = {
    introBtnComponent: <IntroBtnComponent />,
    introOnClick: spyOnClick
  }
  const wrapper = mountWithRouter(<Header {...headerProps} />)
  const introBtnWrapper = wrapper.find('.header__btn')

  introBtnWrapper.simulate('click') // 表示进行点击
  // 一下的断言测试 都是在点击行为发生之后的状态
  expect(spyOnClick).toHaveBeenCalled()
  expect(introBtnWrapper.contains(<IntroBtnComponent />)).toEqual(true)
})

相关文章连接

浏览器端测试:mocha,chai,phantomjs