一. 自动化测试简介

  • 为什么要前端自动化测试:
    自动化测试可以间接的提供代码的测试,多人协作时相互之间未知逻辑的改动等产生的未知或新问题的预警。有效避免一些未考虑到及低级的错误。

  • 自动化测试需要工作:
    自动化测试需要我们手动编写测试代码,当部分逻辑发生改变时,也需要同步更新我们的测试代码。重一定的角度上它也间接的提高了开发及维护成本。这点在实际开发运用中,大家根据实际项目情况来衡量。

  • 前段测试工具概览:
    前端测试工具纷繁复杂,大致分为测试框架, 断言库, 测试覆盖率工具等。

测试框架
测试框架的作用是提供一些方便的语法来描述测试的用例,以及对用例进行分组。
测试框架可分为两种: TDD(测试驱动开发)和 BDD(行为驱动开发)。
常见的测试框架有 Jasmine, Mocha 及 接下来我们要介绍的 Jest

断言库
断言库主要提供语义化方法,用于对参与测试的值做各种各样的判断。 这些语义化方法会返回测试的结果,要么成功,要么失败。
产概念的断言库有Should.js Chai.js

测试覆盖率工具
用于统计测试用例对代码的测试情况, 生成响应的报表。 比如* istanbul *

  • 关于Jest 测试框架概述
    Jest 是Facebook 出品的一个测试框架, 其一大特点是内置了常用的测试工具,比如:自带断言(expect), 测试覆盖率工具(coverage),实现了开箱即用等
    Jest 可以利用其特有的快照测试功能, 通过比对UI代码生成的快照文件, 实现对React等常见框架的自动化测试。
    此外,Jest 测试用例是并行执行的, 而且只执行发生改变的文件所对应的测试,提升了测试速度。

二. Jest 的实践

1. 环境搭建

这里我们主要研究jest的搭建所以,您可以通过官网安装 Create React App 来搭建一个开发环境。
接下来我们需要做如下事情:

  • 安装依赖包
    npm i jest babel-jest -D

  • 添加jest.config.js 文件

// 根目录下创建: jest.config.js

// 配置文档
//https://jestjs.io/docs/zh-Hans/configuration
module.exports = {
  // Automatically clear mock calls and instances between every test
  clearMocks: true,

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // An array of regexp pattern strings used to skip coverage collection
  coveragePathIgnorePatterns: [
    "\\\\node_modules\\\\"
  ],
  
  // An array of file extensions your modules use
  moduleFileExtensions: [
   "js",
   "jsx",
  ],
  
  // A list of paths to directories that Jest should use to search for files in
  roots: null,

  // The test environment that will be used for testing
  testEnvironment: "node",

  // The glob patterns Jest uses to detect test files
  testMatch: [
    "**/__tests__/**/*.js?(x)",
    //"**/?(*.)+(spec|test).js?(x)"
  ],

  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
  testPathIgnorePatterns: [
   "\\\\node_modules\\\\"
  ],
  
  // A map from regular expressions to paths to transformers
  transform: {
    "^.+\\.js$": "babel-jest"
  },

  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
  transformIgnorePatterns: [
    "\\\\node_modules\\\\"
  ],
};
  • 创建测试代码文件目录

在根目录下创建*tests* 文件夹用来存放测试脚本文件。
注: Jest 的测试脚本名形如 **.test.js 或 *.spec.js。 当执行npm run test 命令时,
它会执行当前目录下的所有 的 *.test.js 或 *.spec.js 文件, 完成测试。

  • package.json中添加命令
"scripts":{
    "my-test": "jest --colors --coverage"
}
  • 添加对Jest 的 ES6+支持
    因为jest是基于Node 环境运行。 Node默认对ES6+语法不全支持。所以如果我们用到了ES6+语法,需要为其添加语法支持。
// 为了避免版本冲突,将babel版本全部升级为7,或者将版本全部降到6:   
// 即: @babel/*  7.*  
 "@babel/core": "^7.8.4",
 "@babel/preset-env": "^7.8.4",
 "@babel/preset-react": "^7.8.3",
  "babel-jest": "^24.9.0",
  "babel-loader": "^8.0.6",

2. 用法

  • 用例的表示
    Jest 内部使用了 Jasmin2 来进行测试, 故其用例语法与 Jasmine相同。==test()==函数来描述一个测试用例。
// Demo  普通函数

// src/sum.js
const sum = (a, b) => {
  return a+b
}
export default sum;

// __tests__/demo.test.js
import sum from '../src/sum';

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1,2)).toBe(3)
})

执行命令 npm run my-test 输出结果如下:
在这里插入图片描述
单元测试的几个指标:
%stmts 是语句覆盖率(statement coverage):是不是每个语句都执行了?
%Branch 分支覆盖率(branch coverage):是不是每个if代码块都执行了?
%Funcs 函数覆盖率(function coverage):是不是每个函数都调用了?
%Lines 行覆盖率(line coverage):是不是每一行都执行了?

  • UI 组件测试
// Demo  UI 组件测试

// src/commentItem.js
import React from 'react'

const CommentItem = (props) => (
  <div className={props.list.length>=1?'btn-expand':''}>
    {
      props.list.map((item, index) => {
      return <p key={index}>{item}</p>
      })
    }
  </div>
)
export default CommentItem


// __tests__/demo.ui.test.js

import React from 'react'
import Enzyme, { shallow } from 'enzyme'
import CommentItem from '../src/commentItem'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({ adapter: new Adapter() })

describe('测试评论列表项组件', () => {
  // 这是mocha的玩法,jest可以直接兼容
  it('测试评论内容小于等于200时不出现展开收起按钮', () => {
    const propsData = {
      name: 'hj',
      content: '测试标题',
      list:['l1','l2','l3']
    }
    const item = shallow(<CommentItem {...propsData} />);
    // 这里的断言实际上和chai的expect是很像的
    expect(item.find('.btn-expand').length).toBe(0);
  })
  // 这是jest的玩法,推荐用这种
  test('两数相加结果为两个数字的和', () => {
    expect(3).toBe(3);
  });
})

在这里插入图片描述

扩展
细心的同学应该注意到了,这个实例中用到了==enzyme,Adapter ==。 这里简单说下两者的作用。
(1)Enzyme 简介 传送门

(2)Adapter : 在使用 enzyme 时,需要先适配React版本。

npm i enzyme-adapter-react-16 -D
//使用 
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({ adapter: new Adapter() })

为了避免每次测试文件都这么写, 可以在test目录下新建一个配置文件:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({
  adapter: new Adapter(),
});

export default Enzyme;

然后在测试文件的时候引入这个配置文件即可:

import React from 'react'
import Enzyme from './config/Enzyme.config';
................
const {shallow}=Enzyme

describe('Enzyme的浅渲染测试套件', function () {
  it('Example组件中按钮的名字为text的值', function () {
    const name='按钮名'
    let app = shallow(<Example text={name} />)
    assert.equal(app.find('button').text(),name)
  })
})

3. Jest 之快照测试(Snapshot)

如果你的项目中还没有任何测试用例,那么使用快照测试将是一个最快的基本保障。
如果你想确保你的一些公共组件(UI)不会被意外的被修改变化,那么快照测试是一个非常有用的工具。
它的基本思想是:在测试文件目录下生成快照文件目录“snapshots/**.test.js.snap” 。 每次执行测试命令时,都会与该目录下的对应快照文件进行内容比对。 如果两个图像(内容)不匹配,则测试失败。 除非您同步更新了快照为最新版本(即测试用例中承认且同意修改更新快照内容)。

// snapshot test demo

// src/Link.Snapshot.js
import React from 'react';

const STATUS = {
  HOVERED: 'hovered',
  NORMAL: 'normal',
};

export default class Link extends React.Component {
  constructor() {
    super();

    this.state = {
      class: STATUS.NORMAL,
    };
  }

  _onMouseEnter = () => {
    this.setState({class: STATUS.HOVERED});
  };

  _onMouseLeave = () => {
    this.setState({class: STATUS.NORMAL});
  };

  render() {
    return (
      <a
        className={this.state.class}
        href={this.props.page || '#'}
        onMouseEnter={this._onMouseEnter}
        onMouseLeave={this._onMouseLeave}
      >
        {this.props.children}
      </a>
    );
  }
}


// __tests__/snapshot.test.js
import React from 'react';
import Link from '../src/Link.Snapshot';
import renderer from 'react-test-renderer';

it('renders correctly', () => {
  const tree = renderer.create(
    <Link page="http://www.instagram.com">Instagram</Link>
  ).toJSON();
  expect(tree).toMatchSnapshot();
});

执行测试命令后测试结果如下:
在这里插入图片描述
生成的快照文件内容如下:

// __tests__/__snapshots__/snapshot.test.js.snap

// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.instagram.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Instagram
</a>
`;

当某人不小心修改了我们的公共UI组件代码后(注:测试用例没有修改):

// src/Link.Snapshot.js

...........
................
render() {
    return (
      <a
        className={this.state.class}
        href={ (this.props.page + 'udpate udpate !') || '#'}      //  假设修改了此处, 对href 添加了自定义字符串 ‘udpate udpate !’。
        onMouseEnter={this._onMouseEnter}
        onMouseLeave={this._onMouseLeave}
      >
        {this.props.children}
      </a>
    );
  }
 ...........
 ............................

再次执行测试命令输入结果如下:
在这里插入图片描述
Jest 快照测试通过比对上次的快照输出文件内容,发现不一致。 输出测试失败! 表示该UI组件被修改…

如果我们统一本次的修改, 那么可以通过: npm run my-test – -u 命令来同意同步更新历史快照文件。更新完成后,则测试提示通过。

注: 快照文件应该与代码更改一起提交,并作为代码审查过程的一部分进行审核。
Jest 使用 pretty-format 对快照文件进行了处理,代码会变成可阅读的文件。

3. 常用API

  • Jest 全局方法

Describe(name, fn) : 测试套件,一组相关的测试用例。第一个参数是测试套餐的描述,第二个参数是测试用例。

const my = {
	name : "fynn",
	age : 27
}
describe("my info", ()=>{
	test("my name", ()=>{
		expect(my.name).toBe("fynn")
	});
	test("my age", ()=>{
		expect(my.age).toBe(27)
	})

})
  • Describe.only(name, fn)

当一个file有多个测试套件,但你只想执行其中一个测试套件,可以使用 describe.only。

const my = {
	name : "fynn",
	age : 27
}
let hw = () =>"Hello World!";
describe("my info", ()=>{
	test("my name", ()=>{
		expect(my.name).toBe("fynn")
	});
	test("my age", ()=>{
		expect(my.age).toBe(27)
	})

});
describe.only('hw function test suit',()=>{
	test('hw test',()=>{
		expect(hw()).toBe("Hello World!");
	})
})
  • Describe.skip(name, fn)

一个file中有多个测试套件,如果你想跳过某个测试套件可以使用 describe.skip

  • Test

测试用例,可以写在 describe测试套件中,也可以单独写在测试套件外面

const my = {
   	name : "fynn",
   	age: 27
}
let hw = ()=>"Hello World!"
describe("my info",()=>{
   	test("my name",()=>{
       	expect(my.name).toBe("fynn")
   	});
   	test("my age",()=>{
       	expect(my.age).toBe(27)
   	})
});
test("hw test",()=>{
   	expect(hw()).toBe("Hello World!");
})
  • Test.only

有多个测试用例或测试套件,只想执行其中某一个测试用例时可以用test.only。

const my = {
	name : "fynn",
	age : 27
};
let hw = ()=>"Hello World!";
test("my name",()=>{
	expect(my.name).toBe("fynn");
})
test.only("hw test",()=>{
	expect(hw()).toBe("Hello World!");
})
  • Test.skip(name, fn)

当有多个测试用例,想跳过某个测试用例可以使用test.skip;

  • It(name,fn)

用法和test一样,不能嵌套在test中!可以嵌套在describe中

const my = {
	name : "fynn",
	age : 27
};
let hw = ()=>"Hello World!";
it("my name",()=>{
	expect(my.name).toBe("fynn");
})
xit("hw test",()=>{
	expect(hw()).toBe("Hello World!");
})
  • AfterAll(fn)

当file所有test都执行完毕后,执行afterAll中的方法。

const my = {
	name :"fynn",
	age : 27
};
test("my name",()=>{
	expect(my.name).toBe("fynn")
});
test("my age",()=>{
	expect(my.age).toBe(27)
});
afterAll(()=>{
	console.log("执行完所有test!")
})
  • AfterEach(fn)

每当一个test执行完后,调用一次afterEach中的方法

const my = {
	name :"fynn",
	age : 27
};
test("my name",()=>{
	expect(my.name).toBe("fynn")
});
test("my age",()=>{
	expect(my.age).toBe(27)
});
afterEach(()=>{
	console.log("执行完一个test!")
})
  • BeforeAll(fn)

在所有执行test前先调用beforeAll中的方法

const my = {
   	name :"fynn",
   	age : 27
};
test("my name",()=>{
   	expect(my.name).toBe("fynn")
});
test("my age",()=>{
   	expect(my.age).toBe(27)
});
beforeAll(()=>{
   	console.log("要开始执行test了!")
});
  • BeforeEach(fn)

在每个test执行前都会调用一次beforeEach中的方法

const my = {
   	name :"fynn",
   	age : 27
};
test("my name",()=>{
   	expect(my.name).toBe("fynn")
});
test("my age",()=>{
   	expect(my.age).toBe(27)
});
beforeEach(()=>{
   	console.log("要开始执行一个test了!")
})

其它相关API 参考如下地址:
关于Jest 官方地址 传送门

未完待续…

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐