目录
1、为什么选择 AVA ?
2、API 概览。
3、准备工作。
4、单元测试,测试一个简单的工具函数。
5、使用 Promise、Async/await、Observable 。
6、使用 JSDOM 模拟浏览器环境。
7、单元测试,测试一个简单的 React 组件。
8、Http 接口测试,GitHub 用户信息接口测试。
9、串行测试。
10、快照断言。
11、覆盖率报告:nyc + Coveralls 。
12、持续集成:CircleCI 。
13、学习借鉴,一些使用 AVA 做测试的开源项目。
14、e2e测试框架推荐:TestCafe 。
15、参考。
为什么选择 AVA
原子测试 – 名词的链接属于自己猜测,不知作者本人是否也是表达这个意思。
断言 – 通俗的讲,就是用来判断“ 函数的返回值 ”与我们想要的值是否一致,一致则测试通过,不一致则不通过。
1、轻量,高效,简单。
2、并发测试,强制编写原子测试。
3、没有隐藏的全局变量,每个测试文件独立环境。
4、支持 ES2017,Promise,Generator,Async,Observable。
5、内置断言,强化断言信息。
6、可选的 TAP 输出显示。
7、为什么不用 Mocha,Tape,Tap?
- 官方文档解释:https://github.com/avajs/ava#faq
 - 一些测试框架的对比:https://github.com/koajs/koa/…
 
API 概览
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12  | 
						test([title], implementation)                     基本测试 test.serial([title], implementation)              串行运行测试 test.cb([title], implementation)                  回调函数形式 test.only([title], implementation)                运行指定的测试 test.skip([title], implementation)                跳过测试 test.todo(title)                                  备忘测试 test.failing([title], implementation)             失败的测试 test.before([title], implementation)              钩子函数,这个会在所有测试前运行 test.after([title], implementation)               钩子函数,这个会在所有测试之后运行 test.beforeEach([title], implementation)          钩子函数,这个会在每个测试之前运行 test.afterEach([title], implementation)           钩子函数,这个会在每个测试之后运行 test.after.always([title], implementation)        钩子函数,这个会在所有测试之后运行,不管之前的测试是否失败 test.afterEach.always([title], implementation)    钩子函数,这个会在每个测试之后运行,不管之前的测试是否失败  | 
					
内置断言
也可以用
chai,node assert等其他断言库
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | 
						.pass([message])                                  测试通过 .fail([message])                                  断言失败 .truthy(value, [message])                         断言 value 是否是真值 .falsy(value, [message])                          断言 value 是否是假值 .true(value, [message])                           断言 value 是否是 true .false(value, [message])                          断言 value 是否是 false .is(value, expected, [message])                   断言 value 是否和 expected 相等 .not(value, expected, [message])                  断言 value 是否和 expected 不等 .deepEqual(value, expected, [message])            断言 value 是否和 expected 深度相等 .notDeepEqual(value, expected, [message])         断言 value 是否和 expected 深度不等 .throws(function|promise, [error, [message]])     断言 function 抛出一个异常,或者 promise reject 一个错误 .notThrows(function|promise, [message])           断言 function 没有抛出一个异常,或者 promise resolve .regex(contents, regex, [message])                断言 contents 匹配 regex .notRegex(contents, regex, [message])             断言 contents 不匹配 regex .ifError(error, [message])                        断言 error 是假值 .snapshot(expected, [message])                    将预期值与先前记录的快照进行比较 .snapshot(expected, [options], [message])         将预期值与先前记录的快照进行比较  | 
					
准备工作
务虚已过,编写测试用例之前我们需要先安装 AVA。
先全局安装:npm i --global ava
再在项目根目录安装一次:npm i --save-dev ava
这是通俗的安装方式,全局安装方便 AVA 自身命令行调用,不用太纠结。
像我们刚刚说的,AVA 已经内置支持 ES2017 的语法,安装 AVA 的时候已经帮我们安装了一些关于 babel 的模块,不过我们还再安装几个我们需要用到的 babel 模块,如下。
npm i --save-dev babel-polyfill babel-preset-es2015 babel-preset-react babel-preset-stage-0
| 
					 0 1 2 3  | 
						babel-polyfill                        // 包含 ES2015 及以后的功能函数,如:Object.assign babel-preset-es2015                   // 支持 ES2015 语法 babel-preset-react                    // 支持 React 语法 babel-preset-stage-0                  // 支持 ECMA TC39 对 JS 语言定义的最早一个阶段的想法的语法  | 
					
关于 AVA 的一些基础配置的意思,可以查看一下官方文档。
实际用到的配置也不多,我们在 package.json 文件中配置一下 AVA :
| 
					 0 1 2 3 4 5 6 7 8 9  | 
						"scripts": {   "test": "ava --verbose"             // 添加测试命令,方便我们直接输入一小段命令 npm test。--verbose 表示输出的测试信息尽量详细 }, "ava": {   "babel": "inherit",                 // 继承已有的 babel 配置,就是继承我们下面 .babelrc 的文件配置   "require": [                        // 每个测试前,先加载 require 里面的模块     "babel-register",                 // 默认引入的,安装 AVA 时已经自带安装好     "babel-polyfill"   ] }  | 
					
在项目根目录创建 .babelrc 文件, 并输入以下内容:
这里的坑在于,如果不创建
.babelrc文件,而是把babel的配置写在package.json里,在使用import导入React组件时,会报语法错误。
可使用命令行创建文件:touch .babelrc
| 
					 0 1 2  | 
						{   "presets": ["es2015", "stage-0", "react"] }  | 
					
单元测试,测试一个简单的工具函数
在
test目录创建一个simple_test.js文件,内容如下
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22  | 
						import test from 'ava'; function trimAll(string) {     return string.replace(/[\s\b]/g, ''); } test('trimAll testing', t => {     // 字符串内含有空格符、制表符等空字符都应删除     t.is(trimAll(' \n \r \t \v \b \f B a r r  i  o  r  \n  \r  \t  \v  \b  \f  '), 'Barrior');     // 无空字符时,输出值应为输入值     t.is(trimAll('Barrior'), 'Barrior');     // 输入 new String 对象应与输入基本类型字符串结果相同     t.is(trimAll(new String(' T o m ')), 'Tom');     // 输入其他非字符串数据类型时,应抛出错误     [undefined, null, 0, true, [], {}, () => {}, Symbol()].forEach(type => {         t.throws(() => {             trimAll(type);         });     }); });  | 
					
test():执行一个测试,第一个参数为标题,第二参数为测试用例函数,接收一个包含内置断言 API 的参数 t,也是唯一一个参数;按照惯例这个参数名字叫做 t,没必要重新取名字。
这里使用到的内置断言:
t.is(resultValue, expected), 断言结果值等于我们想要的预期值,则测试通过。全等判断。t.throws(function), 在throws里放入一个函数,函数自动执行,里面执行的结果必须抛出错误,则测试通过。
运行 npm test,可以看到如下结果,一个测试用例通过。

改动一下测试用例,看看测试不通过是怎么样的。
| 
					 0  | 
						t.is(trimAll('Barrior123'), 'Barrior');  | 
					
红色框框就是我们说的强化断言信息,将结果值与预期值进行了差异对比,帮助我们定位错误。
使用 Promise、Async/await、Observable
Promise、Async/await都是语法层面的东西,Observable还没深入了解过,
语法糖的代码就不贴来占用空间了,可以下载示例代码看看就会了。
Observable这里的坑在于需要引入RxJS:npm i --save rxjs,官方文档并没有说明。
| 
					 0 1 2 3 4 5 6 7 8 9 10 11  | 
						import test from 'ava'; import {Observable} from 'rxjs'; test(t => {     t.plan(3);     return Observable         .of(1, 2, 3, 4, 5, 6)         .filter(n => {             return n % 2 === 0;         })         .map(() => t.pass()); });  | 
					
使用 JSDOM 模拟浏览器环境
安装
JSDOM模块:npm i --save-dev jsdom
在目录下创建一个 jsdom.js 文件,内容如下:
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49  | 
						import test from 'ava'; import {JSDOM} from 'jsdom'; const html = ` <!DOCTYPE html> <html> <head></head> <body>     <div class="comment-box">         <textarea></textarea>         <div class="btn">发布</div>         <ul class="list"></ul>     </div>     <script>         const textarea = document.querySelector('.comment-box textarea');         const btn = document.querySelector('.btn');         const list = document.querySelector('.list');         btn.addEventListener('click', () => {             const content = textarea.value;             if (content) {                 const li = document.createElement('li');                 li.innerHTML = content;                 list.insertBefore(li, list.children[0]);                 textarea.value = '';             }         });     </script> </body> </html> `; const {window} = new JSDOM(html, {runScripts: 'dangerously'}); const document = window.document; test('emulate DOM environment with JSDOM', t => {     const textarea = document.querySelector('.comment-box textarea');     const btn = document.querySelector('.btn');     const list = document.querySelector('.list');     const text = 'hello world';     btn.click();                                 // 触发按钮的点击事件,此时文本框中没有输入内容     t.is(list.children.length, 0);               // 列表应该保持为空     textarea.value = text;                       // 文本框中输入内容     btn.click();                                 // 触发按钮的点击事件     t.is(list.children.length, 1);               // 此时列表的长度应该为 1     t.is(list.children[0].innerHTML, text);      // 此时,第一个评论的内容应该等于刚刚我们输入的内容     t.falsy(textarea.value);                     // 评论完后,文本框应该清空    });  | 
					
简单介绍 JSDOM API。
new JSDOM(html, {runScripts: 'dangerously'});:创建一个DOM环境,可以传入完整的HTML文档,也可以值传入一行HTML文档声明,如:<!DOCTYPE html>。- 参数 
runScripts: 'dangerously'表示让文档里的JavaScript可以运行,默认禁止运行。 - 创建后返回一个对象,里面包含一个 
window对象,我们便是需要用到这个window对象,及其属性document对象,用在我们的测试。 - 更多使用方法和配置可以查看一下官方文档。
 
测试里面的代码就是原生的 JavaScript DOM 操作代码。
单元测试,测试一个简单的 React 组件
测试
React组件需要依赖JSDOM, 所以我们放在这里讲。
安装需要依赖的一些模块:npm i --save react react-dom,npm i --save-dev enzyme react-test-renderer。这里也不用纠结为什么一会用--save, 一会用--save-dev, 因为--save表示这些模块在线上项目也需要用到,而--save-dev表示这些模块只用作开发或者测试等,线上项目不需要用到这些模块。
Enzyme是一个React测试工具,可以说是把React组件渲染在我们测试的环境里,不需要依赖真实的浏览器。
Enzyme依赖react-test-renderer,React >=15.5安装react-test-renderer,其它版本安装react-addons-test-utils
在 src 目录下创建 todo.js 文件,内容如下,一个简单的备忘录组件:
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63  | 
						import React from 'react'; import ReactDOM from 'react-dom'; export default class Todo extends React.Component {     constructor(props) {         super(props);         this.state = {             names: props.names || []         };     }     add() {         const elem = this.refs.textarea;         const name = elem.value;         if (name) {             elem.value = '';             this.state.names.push(name);             this.setState({});         } else {             elem.focus();         }     }     del(i) {         this.state.names.splice(i, 1);         this.setState({});     }     render() {         return (             <div className="todo">                 <div>                     <textarea                         cols="30"                         rows="10"                         ref="textarea"                         placeholder="Type member name">                     </textarea>                     <button                         className="btn"                         onClick={this.add.bind(this)}>                         Add member                     </button>                 </div>                 <ul>                     {                         this.state.names.map((name, i) => {                             return (                                 <li key={i}>                                     <span>Member name: {name}</span>                                     <button                                         className="btn"                                         onClick={this.del.bind(this, i)}>                                         Remove member                                     </button>                                 </li>                             )                         })                     }                 </ul>             </div>         )     } }  | 
					
在 test 目录下创建一个 helpers 文件夹,并在文件夹里面创建 setup_dom_env.js 文件, 内容如下。
AVA的规则会忽略helpers文件夹,不会将里面的文件当做测试文件执行。
| 
					 0 1 2 3 4  | 
						import {JSDOM} from 'jsdom'; const dom = new JSDOM('<!DOCTYPE html>'); global.window = dom.window; global.document = dom.window.document; global.navigator = dom.window.navigator;  | 
					
这就是 React 组件需要依赖的 JSDOM 模拟的 DOM 环境的代码。
需要将 window、document、navigator 等对象挂载到 global 对象上,组件才能运行。
在 test 目录下创建 react_component.js, 内容如下,先引入模拟 DOM 环境的文件。
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | 
						import './helpers/setup_dom_env'; import test from 'ava'; import React from 'react'; import {mount} from 'enzyme'; import Todo from '../src/todo'; test('actual testing for react component', t => {     const wrapper = mount(<Todo names={['Barrior', 'Tom']} />);  // 让组件运行,返回一个对象     const list = wrapper.find('ul');                             // 从对象里找到 render 里的 DOM 元素 ul     t.is(list.find('li').length, 2);                             // 断言备忘录有 2 条记录     wrapper.find('textarea').node.value = 'Lily';                // 文本框写入值     wrapper.find('textarea + button').simulate('click');         // 触发按钮的点击事件     t.is(list.find('li').length, 3);                             // 断言备忘录有 3 条记录 });  | 
					
简单介绍 Enzyme API
mount: 表示渲染组件的时候支持生命周期,个人觉得测试时一般都会用这个,因为真实组件生命周期的调用是极为平常的事。Enzyme API和jQuery API很相似,会jQuery应该很容易理解。
Http 接口测试,GitHub 用户信息接口测试
打开接口:https://api.github.com/users/…,返回用户的一些基本信息,有些字段值是动态改变的,用户修改即变,这样的动态字段我们可以查询数据库来对比。这里我们以一个假设不变的
login字段来演示。
先安装 Request 模块: npm i --save-dev request,方便发送 http 请求。
在 test 目录下创建 http.js, 内容如下。
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32  | 
						import test from 'ava'; import request from 'request'; // test.cb() 回调函数形式测试异步代码,异步结束调用 t.end() test.cb('http api testing', t => {     // 基于 Request API 创建 http 请求的配置     const options = {         baseUrl: 'https://api.github.com',         url: '/users/Barrior',         // 请求超时时间         timeout: 5 * 1000,         // http 请求头部,模拟得跟浏览器越像越好,不然被服务器处理成爬虫或者其他就可能得不到我们想要的响应         headers: {             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'         }     };     // Request API 发送 GET 请求     request.get(options, (err, res, body) => {         if (err) t.fail('服务器响应超时!');         if (res && res.statusCode === 200) {             body = JSON.parse(body);             t.is(body.login, 'Barrior');         } else {             t.fail('无响应内容或状态码错误!');         }         // 异步结束         t.end();     }); });  | 
					
串行测试
很多情况并行测试就好,但某些场景我们需要测试按顺序一个接一个的执行,即使是异步,并且后面的测试可能依赖前面测试的结果,这时就需要用到串行测试,
test.serial()。
在 test 目录下创建 serial.js, 内容如下,一个简单的串行测试演示。
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | 
						import test from 'ava'; const globalData = {}; test.serial('serial testing: step one', t => {     return new Promise(resolve => {         setTimeout(() => {             globalData.name = 'Barrior';             t.pass();             resolve();         }, 500);     }); }); test('serial testing: step two', t => {     t.is(globalData.name, 'Barrior'); });  | 
					
这里只是 serial.js 文件串行执行,如果想所有文件都串行执行,需要在命令行传递 --serial 标志。
快照断言
t.snapshot(expected, [options]), 将预期值与先前记录的快照进行比较。
第一次运行测试,快照断言会将预期值存储起来,待第二次及以后运行测试,则拿已经存储好的快照与新的预期值进行比较,吻合则测试通过,否则测试失败。
一般用于预期值比较庞大的情况,如:Html 模板,React 渲染出来的模板,或许还可以用于 Http 接口返回的一堆数据。
如下,做个简单演示。
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33  | 
						import test from 'ava'; function getUserInfo(uid) {     return [{         id: 0,         name: 'Barrior',         sex: 'male'     }, {         id: 1,         name: 'Tom',         sex: 'male'     }][uid] } function renderUserDom(uid) {     const userInfo = getUserInfo(uid);     return `         <div>             <div>${userInfo.name}</div>             <div>${userInfo.sex}</div>             <div>...There are a lot of information</div>         </div>     `; } test('snapshot', t => {     const user1 = renderUserDom(0);     const user2 = renderUserDom(1);     // 自定义 id 必须是一个字符串或者 buffer     // 不定义,AVA 会默认生成一个 id     t.snapshot(user1, {id: '1'});     t.snapshot(user2, {id: '2'}); });  | 
					
覆盖率报告:nyc + Coveralls
安装模块 nyc 和 coveralls:npm i --save-dev nyc coveralls
扩展测试命令,前面加个 nyc 即可:"test": "nyc ava --verbose"
测试覆盖率是基于文件被测试的情况来反馈出指标,所以我们把 simple_test.js 里的 trimAll 函数单独提出来作为一个文件,放到 src 目录,命名为 trim_all.js。
Stmts: Statement 的缩写,语句覆盖,通常指某一行代码是否被测试覆盖了,不包括注释,条件等。
Branch: 分支覆盖或条件覆盖,指某一个条件语句是否被测试覆盖了,如:if、while;分支数是条件语句的两倍。
Funcs: Function 的缩写,函数覆盖,指这个函数是否被测试代码调用了。
Lines: 行覆盖,通常情况等于语句覆盖。一行未必只有一条语句(官方给的差异解释):https://github.com/gotwarlost…
这里有一篇关于这几个指标的具体解释和演示说明,和对做覆盖率报告的思考:http://www.infoq.com/cn/artic…
如果想看具体报告的信息,可以输出成 html 文档来瞧瞧,如下添加输出报告命令。
| 
					 0 1 2 3  | 
						"scripts": {    ...   "report": "nyc report --reporter=html" }  | 
					
运行 npm run report,coverage 目录就会生成一些相关文件,浏览器打开 index.html,就可以看到如下内容。

点击文件进去,可以查看该文件测试覆盖的详情。
Coveralls
一个将项目覆盖率展示到网页上,适合开源项目。
网址:https://coveralls.io
先注册登录,然后在项目根目录添加 .coveralls.yml,内容如下。
| 
					 0 1  | 
						service_name: travis-ci repo_token: 你自己的项目 token, Coveralls 网站提供的私有令牌  | 
					
添加上传命令。
| 
					 0 1 2 3  | 
						"scripts": {    ...   "coverage": "nyc report --reporter=text-lcov | coveralls" }  | 
					
运行 npm run coverage,等待报告上传完毕,就可以在网站上看到报告。
持续集成:CircleCI
通俗的讲,持续集成就是每次提交代码,自动化程序就自动构建(包括编译,发布,自动化测试等)来验证代码,从而尽早地发现代码中的错误。
网址:https://circleci.com/,适合开源项目。
在项目根目录添加 circle.yml 文件,内容如下,配置项都可以在文档中找到。
| 
					 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14  | 
						# 配置 NodeJS 的版本为 7 machine:   node:     version: 7 # 安装依赖的命令 dependencies:   override:     - npm i -g ava     - npm i # 运行的测试命令 test:   override:     - npm test  | 
					
使用 GitHub 账号登录 CircleCI 网站,选择持续集成这个项目,这里我们用的是 1.0 平台,不要选 2.0,因为配置的写法不一样。
至此,每次提交代码到这个项目,CircleCI 就会自动帮我们集成。

完成了覆盖率和持续集成,这两个网站都提供了小徽章给我们,类似如下,可以贴到项目中以显某种态度。
![]()
学习借鉴,一些使用 AVA 做测试的开源项目
e2e测试框架推荐:TestCafe
推荐理由(缺点须躬行):
- 无需配置繁琐的环境。
 - 基于 
NodeJS生态。 
参考
http://i5ting.github.io/ava-p…
https://github.com/avajs/ava
最后
文中的代码托放于 GitHub,可供参考。


