自動(dòng)化測試之前端js單元測試框架jest

大多數(shù)開發(fā)者都知道需要寫單元測試,但是不知道每個(gè)單元測試應(yīng)用的主要內(nèi)容以及如何做單元測試,在介紹jest測試框架前,我們先來了解下一些測試相關(guān)的概念。

為什么需要單元測試?

  • 保證質(zhì)量:隨著迭代的過程,開發(fā)人員很難記清所有的功能點(diǎn),功能點(diǎn)的新增和刪除在代碼改變后,進(jìn)行回歸測試時(shí),依靠人工QA很容易出錯(cuò)遺漏。
  • 自動(dòng)化:通過編寫測試用例,只需要編寫一次,多次運(yùn)行,同樣的事情不需要從頭再來測一遍,很多時(shí)候QA的工作量就是這么增加的,新的版本上線,人工QA都需要所有的功能點(diǎn)從新測試一遍。
  • 特性文檔:單元測試可以作為描述和記錄代碼所實(shí)現(xiàn)的所有需求,有時(shí)候可以作為文檔來使用,了解一個(gè)項(xiàng)目可以通過閱讀測試用例比看需求文檔更清晰。
  • 驅(qū)動(dòng)開發(fā),指導(dǎo)設(shè)計(jì):代碼被測試的前提是代碼本身的可測試性,那么要保證代碼的可測試性,就需要在開發(fā)中注意API的設(shè)計(jì),TDD將測試前移就是起到這么一個(gè)作用

測試類型

你可能接觸過各種測試框架、大體上,最重要測試類型有:

  • 單元測試- 依靠模擬輸入證實(shí)是否是期望的輸出來分別的測試函數(shù)或者類。
  • 集成測試 - 測試若干模塊來確保他們像預(yù)期的那樣工作。
  • 功能測試- 在產(chǎn)品本身(例如在瀏覽器上)對一個(gè)場景進(jìn)行操作,而不考慮內(nèi)部結(jié)構(gòu)以確保預(yù)期的行為。

測試工具類型

測試工具可以分為以下功能,有些提供一個(gè)功能,有些提供了一個(gè)組合。
使用工具組合是很常見的,即使你可以使用單一的工具實(shí)現(xiàn)同樣的功能,是所有組合可以獲得更靈活的功能。

  1. 測試環(huán)境(Mocha , Jasmine, Jest, Karma
  2. 測試結(jié)構(gòu) (Mocha , Jasmine, Jest, Cucumber
  3. 斷言函數(shù)(Chai, Jasmine, Jest, Unexpected
  4. 生成,顯示、監(jiān)聽測試結(jié)果(Mocha , Jasmine, JestKarma
  5. 生成,比較組件和數(shù)據(jù)結(jié)構(gòu)的快照,以確保之前運(yùn)行的更改是預(yù)期的。(Jest,Ava
  6. mocks。(sinon.js) 目前使用最多的mock庫,將其分為spies、stub、fake XMLHttpRequest、Fake server、Fake time幾種,根據(jù)不同的場景進(jìn)行選擇。
  7. 生成代碼覆蓋率報(bào)告。(Istanbul, Jest
  8. 瀏覽器或者類瀏覽器環(huán)境執(zhí)行控制。(Protractor , Nightwatch, Phantom, Casper

單元測試技術(shù)的實(shí)現(xiàn)原理

  1. 測試框架:判斷內(nèi)部是否存在異常,存在則console出對應(yīng)的text信息
  2. 斷言庫:當(dāng)actual值與expect值不一樣時(shí),就拋出異常,供外部測試框架檢測到,這就是為什么有些測試框架可以自由選擇斷言庫的原因,只要可以拋出異常,外部測試框架就可以工作。
  3. mock函數(shù):創(chuàng)建一個(gè)新的函數(shù),用這個(gè)函數(shù)來取代原來的函數(shù),同時(shí)在這個(gè)新函數(shù)上添加一些額外的屬性,例如called、calledWithArguments等信息

function describe (text, fn) {
    try {
        fn.apply(...);
    } catch(e) {
        assert(text)
    }
}
function fn () {
    while (...) {
        beforeEach();
        it(text, function () {
            assert();
        });
        afterEach();
    }
}
function it(text, fn) {
  ...
  fn(text)
  ...
}
function assert (expect, actual) {
    if (expect not equla actual ) {
        throw new Error(text);
    }
}
function fn () {
  ...
}

function spy(cb) {
  var proxy = function () {
    ...
  }
  proxy.called = false;
  proxy.returnValue = '...';
  ...
  return proxy;
}

var proxy = spy(fn); // 得到一個(gè)mock函數(shù)

測試用例的鉤子

describe塊之中,提供測試用例的四個(gè)鉤子:before()、after()、beforeEach()和afterEach()。它們會(huì)在指定時(shí)間執(zhí)行。

describe('hooks', function() {

  before(function() {
    // 在本區(qū)塊的所有測試用例之前執(zhí)行
  });

  after(function() {
    // 在本區(qū)塊的所有測試用例之后執(zhí)行
  });

  beforeEach(function() {
  // 在本區(qū)塊的每個(gè)測試用例之前執(zhí)行
    this.closeFunc = sinon.stub();
    this.Modal = TestUtils.renderIntoDocument(
      <Modal title="whatever" handleClose={this.closeFunc}>
        <div className="m-content">
          <p className="m-text">Just some noddy content</p>
          <a href="#" className="other-link">Click me</a>
        </div>
      </Modal>
    );
    this.eventStub = {
      preventDefault: sinon.stub(),
      stopPropagation: sinon.stub(),
    };
  });

  afterEach(function() {
    // 在本區(qū)塊的每個(gè)測試用例之后執(zhí)行
  });

  // test cases
  
  it('should have a title', function() {
    var title = helpers.findByTag(this.Modal, 'h2');
    assert.equal(findDOMNode(title).firstChild.nodeValue, 'whatever');
  });

  it('should have child content', function() {
    var content = helpers.findByClass(this.Modal, 'm-content');
    assert.equal(findDOMNode(content).nodeName.toLowerCase(), 'div');
  });

  it('should have child paragraph', function() {
    var text = helpers.findByClass(this.Modal, 'm-text');
    assert.equal(findDOMNode(text).firstChild.nodeValue,
                 'Just some noddy content');
  });


});

如何寫單元測試用例

一些好的建議:

  • 只考慮測試,不考慮內(nèi)部實(shí)現(xiàn)
  • 不要做無謂的斷言
  • 讓每個(gè)單元測試保持獨(dú)立
  • 所有的方法都應(yīng)該寫單元測試
  • 充分考慮數(shù)據(jù)的邊界條件
  • 對重點(diǎn)、復(fù)雜、核心代碼,重點(diǎn)測試
  • 利用AOP(beforeEach、afterEach),減少測試代碼數(shù)量,避免無用功能
  • 使用最合適的斷言方式

TDD
一句話簡單來說,就是先寫測試,后寫功能實(shí)現(xiàn)。TDD的目的是通過測試用例來指引實(shí)際的功能開發(fā),讓開發(fā)人員首先站在全局的視角來看待需求。具體定義可以查看維基;

BDD
行為驅(qū)動(dòng)開發(fā)要求更多人員參與到軟件的開發(fā)中來,鼓勵(lì)開發(fā)者、QA、相關(guān)業(yè)務(wù)人員相互協(xié)作。BDD是由商業(yè)價(jià)值來驅(qū)動(dòng),通過用戶接口(例如GUI)理解應(yīng)用程序。詳見維基.
<blockquote>


Jest介紹--Painless JavaScript Testing

Jest 是一款 Facebook 開源的 JS 單元測試框架,目前 Jest 已經(jīng)在 Facebook 開源的 React, React Native 等前端項(xiàng)目中被做為標(biāo)配測試框架。

Jest功能:

  • 內(nèi)置Jasmin語法
  • 內(nèi)置auto mock
  • 自帶mock API
  • 前端友好(集成JSDOM)
  • 支持直接使用Promise和async/await書寫異步代碼
  • 支持對 React 組件進(jìn)行快照監(jiān)控
  • 擴(kuò)展和集成 Babel 等常用工具集也很方便
  • 自動(dòng)環(huán)境隔離

Jest用法

安裝:

npm install --save-dev jest

package.json中添加:

{
  "scripts": {
    "test": "jest"
  }
}

運(yùn)行 npm test

也可通過命令行運(yùn)行:
jest my-test --notify --config=config.json

附加配置

npm install --save-dev babel-jest regenerator-runtime

項(xiàng)目根目錄添加.babelrc文件

{
  "presets": ["es2015", "react"]
}

Jest自動(dòng)定義 NODE_ENV = test

測試腳本的寫法

下面是一個(gè)加法模塊add.js
的代碼。

// add.js
function add(x, y) {
  return x + y;
}

module.exports = add;

要測試這個(gè)加法模塊是否正確,就要寫測試腳本。
通常,測試腳本與所要測試的源碼腳本同名,但是后綴名為.test.js
(表示測試)或者.spec.js
(表示規(guī)格)。比如,add.js
的測試腳本名字就是add.test.js
。

import add from '../src/add'
describe('加法函數(shù)測試', () => {
    it('1加2應(yīng)該等于3', () => {
        expect(add(1, 2)).toBe(3);
    });
});
  • 測試腳本里面應(yīng)該包括一個(gè)或多個(gè)describe塊,每個(gè)describe塊應(yīng)該包括一個(gè)或多個(gè)it塊。

  • describe塊稱為"測試套件"(test suite),表示一組相關(guān)的測試。它是一個(gè)函數(shù),第一個(gè)參數(shù)是測試套件的名稱("加法函數(shù)的測試"),第二個(gè)參數(shù)是一個(gè)實(shí)際執(zhí)行的函數(shù)。

  • it塊稱為"測試用例"(test case),表示一個(gè)單獨(dú)的測試,是測試的最小單位。它也是一個(gè)函數(shù),第一個(gè)參數(shù)是測試用例的名稱("1 加 1 應(yīng)該等于 2"),第二個(gè)參數(shù)是一個(gè)實(shí)際執(zhí)行的函數(shù)。

斷言庫的用法

  expect(add(1, 2)).toBe(3);

所謂"斷言",就是判斷源碼的實(shí)際執(zhí)行結(jié)果與預(yù)期結(jié)果是否一致,如果不一致就拋出一個(gè)錯(cuò)誤。上面這句斷言的意思是,調(diào)用add(1, 1),結(jié)果應(yīng)該等于2。

所有的測試用例(it塊)都應(yīng)該含有一句或多句的斷言。它是編寫測試用例的關(guān)鍵。斷言功能由斷言庫來實(shí)現(xiàn)

簡單測試

// add.js
function add(a, b) {
    return a + b;
}
module.exports = add;

// add.test.js
import add from '../src/add'
describe('加法函數(shù)測試', () => {
    it('1加2應(yīng)該等于3', () => {
        expect(add(1, 2)).toBe(3);
    });
});

異步的單元測試

// user.js
import request from './request';
export function getUserName(userID) {
  return request('/users/' + userID).then(user => user.name);
}
// user.test.js

import  * as user from 'user';
// 普通回調(diào)
it('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }
  fetchData(callback);
});

// 方法需要返回一個(gè)promise對象
it('works with promises',() => {
  return user.getUserName(5)
      .then(name => expect(name).toEqual('Paul'));
});

// async/await
it('works with async/await', async () => {
  const userName = await user.getUserName(4);
  expect(userName).toEqual('Mark');
})

React組件的單元測試

// CheckboxWidthLabel.js
import React from 'react';

export default class CheckboxWithLabel extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isChecked: false };
        this.onChange = this.onChange.bind(this);
    }

    onChange() {
        this.setState({ isChecked: !this.state.isChecked });
    }

    render() {
        return (
            <label >
                <input 
                type = "checkbox"
                checked = { this.state.isChecked }
                onChange = { this.onChange }
                /> 
                { this.state.isChecked ? this.props.labelOn : this.props.labelOff }
            </label >
        )
    }
}

//CheckboxWithLabel-test.js

import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-dom/test-utils';
import CheckboxWidthLabel from 'CheckboxWithLabel';

it('CheckboxWithlabel changes the text after click', () => {
    const checkbox = TestUtils.renderIntoDocument( <
        CheckboxWidthLabel labelOn = "On"
        labelOff = "Off" / >
    );
    const checkboxNode = ReactDOM.findDOMNode(checkbox);
    expect(checkboxNode.textContent).toEqual('Off');
    TestUtils.Simulate.change(
        TestUtils.findRenderedDOMComponentWithTag(checkbox, 'input')
    )
    expect(checkboxNode.textContent).toEqual('On');
})

手動(dòng)mock

Paste_Image.png

__ _ _ mocks __ _ _/fetch.js

const actions = {
  "GetAnnounce":{data:["公告公告公告","公告2公告2公告2"]}
}
export default function fetch(params){
  return new Promise((resolve, reject) => {
    const actionType = arams.url.substr('/common/'.length)
    const res = actions[actionType];
      process.nextTick(
        () => res ? resolve(res) : reject({
          error: 'action with ' + actionType + ' not found.',
        })
      );
  })
}

fetch.js


import fetch from './fetch'
export function fetchNotice(params){
  return fetch({
    url:'/common/GetAnnounce',
    params:params|{}
  }).then(annouce => annouce)
}
export function getAllNotice(params){
  return params || {}
}

annouce.js

import fetch from './fetch'
export function fetchNotice(params){
  return fetch({
    url:'/common/GetAnnounce',
    params:params|{}
  }).then(annouce => annouce)
}
export function getAllNotice(params){
  return params || {}
}

annouce.test.js

jest.mock('services/fetch')
import {fetchNotice} from 'services/annouce'

describe('annouce',() => {
  describe('獲取公告列表', () => {
      it('正確返回公告數(shù)組',  () => {
        // expect([1,2]).toEqual([1,2])
      return fetchNotice().then(res => expect(res.data).toHaveLength(2)).catch(err => console.log(err))
      });
      it('正確返回公告數(shù)組',  () => {
        // expect([1,2]).toEqual([1,2])
      return fetchNotice().then(res => expect(res.data).toBeTruthy())
      });
      it('正確返回公告數(shù)組',  () => {
        // expect([1,2]).toEqual([1,2])
      return fetchNotice().then(res => expect(res.data).toContain('公告公告公告'))
      });
  })
})

注意:

  1. ___mocks____文件夾要和要mock的方法放在同一級(jí)目錄。

2.如果mock的是nodejs方法,____ mocks ____文件夾要放在項(xiàng)目根目錄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 大多數(shù)的iOS App (沒有持續(xù)集成)迭代流程是這樣的: 也就是說,測試是發(fā)布之前的最后一道關(guān)卡。如果bug不能...
    伯牙呀閱讀 4,979評(píng)論 1 22
  • 前言 本篇文章是我在學(xué)習(xí)前端自動(dòng)化單元測試時(shí)的一些思路整理,之前也從未接觸過單元測試相關(guān)工具,如有錯(cuò)漏,請讀者斧正...
    Awey閱讀 12,895評(píng)論 8 37
  • 非常認(rèn)可這句話:自動(dòng)化測試是為了提高效率,測試腳本要易維護(hù),不能讓測試腳本變成另一種技術(shù)債務(wù),不能為了自動(dòng)化測試而...
    Kewings閱讀 8,234評(píng)論 0 10
  • Instrumentation介紹 Instrumentation是個(gè)什么東西? Instrumentation測...
    打不死的小強(qiáng)qz閱讀 7,953評(píng)論 2 39
  • 前言 如果有測試大佬發(fā)現(xiàn)內(nèi)容不對,歡迎指正,我會(huì)及時(shí)修改。 大多數(shù)的iOS App(沒有持續(xù)集成)迭代流程是這樣的...
    默默_David閱讀 1,772評(píng)論 0 4

友情鏈接更多精彩內(nèi)容