一、为何选择 AVA ?
二、API 概览。
三、准备工做。
四、单元测试,测试一个简单的工具函数。
五、使用 Promise、Async/await、Observable 。
六、使用 JSDOM 模拟浏览器环境。
七、单元测试,测试一个简单的 React 组件。
八、Http 接口测试,GitHub 用户信息接口测试。
九、串行测试。
十、快照断言。
十一、覆盖率报告:nyc + Coveralls 。
十二、持续集成:CircleCI 。
1三、学习借鉴,一些使用 AVA 作测试的开源项目。
1四、e2e测试框架推荐:TestCafe 。
1五、参考。css
原子测试 - 名词的连接属于本身猜想,不知做者本人是否也是表达这个意思。
断言 - 通俗的讲,就是用来判断“ 函数的返回值 ”
与咱们想要的值是否一致,一致则测试经过,不一致则不经过。html
一、轻量,高效,简单。
二、并发测试,强制编写原子测试。
三、没有隐藏的全局变量,每一个测试文件独立环境。
四、支持 ES2017,Promise,Generator,Async,Observable。
五、内置断言,强化断言信息。
六、可选的 TAP 输出显示。
七、为何不用 Mocha,Tape,Tap?node
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
等其余断言库react
.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 自身命令行调用,不用太纠结。git
像咱们刚刚说的,AVA
已经内置支持 ES2017
的语法,安装 AVA
的时候已经帮咱们安装了一些关于 babel
的模块,不过咱们还再安装几个咱们须要用到的 babel
模块,以下。npm i --save-dev babel-polyfill babel-preset-es2015 babel-preset-react babel-preset-stage-0
github
babel-polyfill // 包含 ES2015 及之后的功能函数,如:Object.assign babel-preset-es2015 // 支持 ES2015 语法 babel-preset-react // 支持 React 语法 babel-preset-stage-0 // 支持 ECMA TC39 对 JS 语言定义的最先一个阶段的想法的语法
关于 AVA
的一些基础配置的意思,能够查看一下官方文档。
实际用到的配置也很少,咱们在 package.json
文件中配置一下 AVA
:数据库
"scripts": { "test": "ava --verbose" // 添加测试命令,方便咱们直接输入一小段命令 npm test。--verbose 表示输出的测试信息尽可能详细 }, "ava": { "babel": "inherit", // 继承已有的 babel 配置,就是继承咱们下面 .babelrc 的文件配置 "require": [ // 每一个测试前,先加载 require 里面的模块 "babel-register", // 默认引入的,安装 AVA 时已经自带安装好 "babel-polyfill" ] }
在项目根目录建立 .babelrc
文件, 并输入如下内容:express
这里的坑在于,若是不建立
.babelrc
文件,而是把babel
的配置写在package.json
里,在使用import
导入React
组件时,会报语法错误。
可以使用命令行建立文件:touch .babelrc
npm
{ "presets": ["es2015", "stage-0", "react"] }
看看如今的目录结构是怎么样的:json
在
test
目录建立一个simple_test.js
文件,内容以下
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
,能够看到以下结果,一个测试用例经过。
改动一下测试用例,看看测试不经过是怎么样的。
t.is(trimAll('Barrior123'), 'Barrior');
运行 npm test
红色框框就是咱们说的强化断言信息
,将结果值
与预期值
进行了差别对比,帮助咱们定位错误。
Promise
、Async/await
都是语法层面的东西,Observable
还没深刻了解过,
语法糖的代码就不贴来占用空间了,能够下载示例代码
看看就会了。Observable
这里的坑在于须要引入RxJS
:npm i --save rxjs
,官方文档并无说明。
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
模块:npm i --save-dev jsdom
在目录下建立一个 jsdom.js
文件,内容以下:
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
组件须要依赖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
文件,内容以下,一个简单的备忘录组件:
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
文件夹,不会将里面的文件当作测试文件执行。
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
环境的文件。
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
应该很容易理解。打开接口:https://api.github.com/users/...,返回用户的一些基本信息,有些字段值是动态改变的,用户修改即变,这样的动态字段咱们能够查询数据库来对比。这里咱们以一个假设不变的
login
字段来演示。
先安装 Request
模块: npm i --save-dev request
,方便发送 http
请求。
在 test
目录下建立 http.js
, 内容以下。
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(); }); });
运行 npm test
,能够看到测试经过。
不少状况并行测试就好,但某些场景咱们须要测试按顺序一个接一个的执行,即便是异步,而且后面的测试可能依赖前面测试的结果,这时就须要用到串行测试,
test.serial()
。
在 test
目录下建立 serial.js
, 内容以下,一个简单的串行测试演示。
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
接口返回的一堆数据。
以下,作个简单演示。
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 class="user-info"> <div class="name">${userInfo.name}</div> <div class="sex">${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
:npm i --save-dev nyc coveralls
扩展测试命令,前面加个 nyc
便可:"test": "nyc ava --verbose"
测试覆盖率是基于文件被测试的状况来反馈出指标,因此咱们把 simple_test.js
里的 trimAll
函数单独提出来做为一个文件,放到 src
目录,命名为 trim_all.js
。
运行 npm test
,简洁的覆盖率报告以下。
Stmts
: Statement 的缩写,语句覆盖,一般指某一行代码是否被测试覆盖了,不包括注释,条件等。Branch
: 分支覆盖或条件覆盖,指某一个条件语句是否被测试覆盖了,如:if
、while
;分支数是条件语句的两倍。Funcs
: Function 的缩写,函数覆盖,指这个函数是否被测试代码调用了。Lines
: 行覆盖,一般状况等于语句覆盖。一行未必只有一条语句(官方给的差别解释):https://github.com/gotwarlost...
这里有一篇关于这几个指标的具体解释和演示说明,和对作覆盖率报告的思考:http://www.infoq.com/cn/artic...
若是想看具体报告的信息,能够输出成 html
文档来瞧瞧,以下添加输出报告命令。
"scripts": { ... "report": "nyc report --reporter=html" }
运行 npm run report
,coverage
目录就会生成一些相关文件,浏览器打开 index.html
,就能够看到以下内容。
点击文件进去,能够查看该文件测试覆盖的详情。
一个将项目覆盖率展现到网页上,适合开源项目。
网址:https://coveralls.io
先注册登陆,而后在项目根目录添加 .coveralls.yml
,内容以下。
service_name: travis-ci repo_token: 你本身的项目 token, Coveralls 网站提供的私有令牌
添加上传命令。
"scripts": { ... "coverage": "nyc report --reporter=text-lcov | coveralls" }
运行 npm run coverage
,等待报告上传完毕,就能够在网站上看到报告。
通俗的讲,持续集成就是每次提交代码,自动化程序就自动构建(包括编译,发布,自动化测试等)来验证代码,从而尽早地发现代码中的错误。
网址:https://circleci.com/,适合开源项目。
在项目根目录添加 circle.yml
文件,内容以下,配置项均可以在文档中找到。
# 配置 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
就会自动帮咱们集成。
完成了覆盖率和持续集成,这两个网站都提供了小徽章给咱们,相似以下,能够贴到项目中以显某种态度。
推荐理由(缺点须躬行):
NodeJS
生态。http://i5ting.github.io/ava-p...
https://github.com/avajs/ava
文中的代码托放于 GitHub
,可供参考。