自動化測試之前端js單元測試框架jest

大多數開發者都知道需要寫單元測試,但是不知道每個單元測試應用的主要內容以及如何做單元測試,在介紹jest測試框架前,我們先來了解下一些測試相關的概念。

為什么需要單元測試?

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

測試類型

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

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

測試工具類型

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

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

單元測試技術的實現原理

  1. 測試框架:判斷內部是否存在異常,存在則console出對應的text信息
  2. 斷言庫:當actual值與expect值不一樣時,就拋出異常,供外部測試框架檢測到,這就是為什么有些測試框架可以自由選擇斷言庫的原因,只要可以拋出異常,外部測試框架就可以工作。
  3. mock函數:創建一個新的函數,用這個函數來取代原來的函數,同時在這個新函數上添加一些額外的屬性,例如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); // 得到一個mock函數

測試用例的鉤子

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

describe('hooks', function() {

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

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

  beforeEach(function() {
  // 在本區塊的每個測試用例之前執行
    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() {
    // 在本區塊的每個測試用例之后執行
  });

  // 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');
  });


});

如何寫單元測試用例

一些好的建議:

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

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

BDD
行為驅動開發要求更多人員參與到軟件的開發中來,鼓勵開發者、QA、相關業務人員相互協作。BDD是由商業價值來驅動,通過用戶接口(例如GUI)理解應用程序。詳見維基.
<blockquote>


Jest介紹--Painless JavaScript Testing

Jest 是一款 Facebook 開源的 JS 單元測試框架,目前 Jest 已經在 Facebook 開源的 React, React Native 等前端項目中被做為標配測試框架。

Jest功能:

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

Jest用法

安裝:

npm install --save-dev jest

package.json中添加:

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

運行 npm test

也可通過命令行運行:
jest my-test --notify --config=config.json

附加配置

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

項目根目錄添加.babelrc文件

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

Jest自動定義 NODE_ENV = test

測試腳本的寫法

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

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

module.exports = add;

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

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

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

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

斷言庫的用法

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

所謂"斷言",就是判斷源碼的實際執行結果與預期結果是否一致,如果不一致就拋出一個錯誤。上面這句斷言的意思是,調用add(1, 1),結果應該等于2。

所有的測試用例(it塊)都應該含有一句或多句的斷言。它是編寫測試用例的關鍵。斷言功能由斷言庫來實現

簡單測試

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

// add.test.js
import add from '../src/add'
describe('加法函數測試', () => {
    it('1加2應該等于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';
// 普通回調
it('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }
  fetchData(callback);
});

// 方法需要返回一個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');
})

手動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('正確返回公告數組',  () => {
        // expect([1,2]).toEqual([1,2])
      return fetchNotice().then(res => expect(res.data).toHaveLength(2)).catch(err => console.log(err))
      });
      it('正確返回公告數組',  () => {
        // expect([1,2]).toEqual([1,2])
      return fetchNotice().then(res => expect(res.data).toBeTruthy())
      });
      it('正確返回公告數組',  () => {
        // expect([1,2]).toEqual([1,2])
      return fetchNotice().then(res => expect(res.data).toContain('公告公告公告'))
      });
  })
})

注意:

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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