一、爲何選擇 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
,可供參考。