使用 AVA 作自動化測試

目錄

一、爲何選擇 AVA ?
二、API 概覽。
三、準備工做。
四、單元測試,測試一個簡單的工具函數。
五、使用 Promise、Async/await、Observable 。
六、使用 JSDOM 模擬瀏覽器環境。
七、單元測試,測試一個簡單的 React 組件。
八、Http 接口測試,GitHub 用戶信息接口測試。
九、串行測試。
十、快照斷言。
十一、覆蓋率報告:nyc + Coveralls 。
十二、持續集成:CircleCI 。
1三、學習借鑑,一些使用 AVA 作測試的開源項目。
1四、e2e測試框架推薦:TestCafe 。
1五、參考。css

爲何選擇 AVA

原子測試 - 名詞的連接屬於本身猜想,不知做者本人是否也是表達這個意思。
斷言 - 通俗的講,就是用來判斷 「 函數的返回值 」 與咱們想要的值是否一致,一致則測試經過,不一致則不經過。html

一、輕量,高效,簡單。
二、併發測試,強制編寫原子測試
三、沒有隱藏的全局變量,每一個測試文件獨立環境。
四、支持 ES2017,Promise,Generator,Async,Observable。
五、內置斷言,強化斷言信息。
六、可選的 TAP 輸出顯示
七、爲何不用 Mocha,Tape,Tap?node

  1. 官方文檔解釋:https://github.com/avajs/ava#faq
  2. 一些測試框架的對比:https://github.com/koajs/koa/...

API 概覽

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-0github

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 .babelrcnpm

{
  "presets": ["es2015", "stage-0", "react"]
}

看看如今的目錄結構是怎麼樣的:
clipboard.pngjson

單元測試,測試一個簡單的工具函數

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,能夠看到以下結果,一個測試用例經過。
clipboard.png

改動一下測試用例,看看測試不經過是怎麼樣的。

t.is(trimAll('Barrior123'), 'Barrior');

運行 npm test
clipboard.png

紅色框框就是咱們說的強化斷言信息,將結果值預期值進行了差別對比,幫助咱們定位錯誤。

使用 Promise、Async/await、Observable

PromiseAsync/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 模擬瀏覽器環境

安裝 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 組件

測試 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-rendererReact >=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 環境的代碼。
須要將 windowdocumentnavigator 等對象掛載到 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 APIjQuery API 很類似,會 jQuery 應該很容易理解。

Http 接口測試,GitHub 用戶信息接口測試

打開接口: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,能夠看到測試經過。
clipboard.png

串行測試

不少狀況並行測試就好,但某些場景咱們須要測試按順序一個接一個的執行,即便是異步,而且後面的測試可能依賴前面測試的結果,這時就須要用到串行測試,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

安裝模塊 nyccoverallsnpm i --save-dev nyc coveralls
擴展測試命令,前面加個 nyc 便可:"test": "nyc ava --verbose"
測試覆蓋率是基於文件被測試的狀況來反饋出指標,因此咱們把 simple_test.js 裏的 trimAll 函數單獨提出來做爲一個文件,放到 src 目錄,命名爲 trim_all.js

運行 npm test,簡潔的覆蓋率報告以下。
clipboard.png

Stmts: Statement 的縮寫,語句覆蓋,一般指某一行代碼是否被測試覆蓋了,不包括註釋,條件等。
Branch: 分支覆蓋或條件覆蓋,指某一個條件語句是否被測試覆蓋了,如:ifwhile;分支數是條件語句的兩倍。
Funcs: Function 的縮寫,函數覆蓋,指這個函數是否被測試代碼調用了。
Lines: 行覆蓋,一般狀況等於語句覆蓋。一行未必只有一條語句(官方給的差別解釋):https://github.com/gotwarlost...

這裏有一篇關於這幾個指標的具體解釋和演示說明,和對作覆蓋率報告的思考:http://www.infoq.com/cn/artic...

若是想看具體報告的信息,能夠輸出成 html 文檔來瞧瞧,以下添加輸出報告命令。

"scripts": {
   ...
  "report": "nyc report --reporter=html"
}

運行 npm run reportcoverage 目錄就會生成一些相關文件,瀏覽器打開 index.html,就能夠看到以下內容。
clipboard.png

點擊文件進去,能夠查看該文件測試覆蓋的詳情。

Coveralls

一個將項目覆蓋率展現到網頁上,適合開源項目。
網址:https://coveralls.io

先註冊登陸,而後在項目根目錄添加 .coveralls.yml,內容以下。

service_name: travis-ci
repo_token: 你本身的項目 token, Coveralls 網站提供的私有令牌

添加上傳命令。

"scripts": {
   ...
  "coverage": "nyc report --reporter=text-lcov | coveralls"
}

運行 npm run coverage,等待報告上傳完畢,就能夠在網站上看到報告。

持續集成:CircleCI

通俗的講,持續集成就是每次提交代碼,自動化程序就自動構建(包括編譯,發佈,自動化測試等)來驗證代碼,從而儘早地發現代碼中的錯誤。
網址: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 就會自動幫咱們集成。
clipboard.png

完成了覆蓋率和持續集成,這兩個網站都提供了小徽章給咱們,相似以下,能夠貼到項目中以顯某種態度。
clipboard.png

學習借鑑,一些使用 AVA 作測試的開源項目

e2e測試框架推薦:TestCafe

官網地址:https://devexpress.github.io/...

推薦理由(缺點須躬行):

  1. 無需配置繁瑣的環境。
  2. 基於 NodeJS 生態。

參考

http://i5ting.github.io/ava-p...
https://github.com/avajs/ava

最後

文中的代碼託放於 GitHub,可供參考。

相關文章
相關標籤/搜索