前端單元測試

前端單元測試概述

前端測試工具

測試分爲e2e測試和單元測試和集成測試html

e2e:端到端的測試,主要是測業務,絕大部分狀況是指在瀏覽器上對某一個網站的某一個功能進行操做。前端

單元測試工具:mache、ava、jest、jasmine等vue

斷言庫: shoud.js.chai.js 等node

測試覆蓋率工具:istanbulreact

react 採用jest加enzyne的寫法 e2e 測試pupertearandroid

vue 採用jest e2e 適應nightwatch 的方案正則表達式

測試分爲三個種類

  • 單元測試npm

    在計算機編程中,單元測試(英語:Unit Testing)又稱爲模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工做。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法
  • 集成測試編程

    集成測試,也叫組裝測試或聯合測試。在單元測試的基礎上,將全部模塊按照設計要求(如根據結構圖)組裝成爲子系統或系統,進行集成測試。
  • 功能測試json

    功能測試就是對產品的各功能進行驗證,根據功能測試用例,逐項測試,檢查產品是否達到用戶要求的功能。

單元測試和集成測試簡單對比

React & Redux 應用構建在三個基本的構建塊上:actions、reducers 和 components。是獨立測試它們(單元測試),仍是一塊兒測試(集成測試)取決於你。集成測試會覆蓋到整個功能,能夠把它想成一個黑盒子,而單元測試專一於特定的構建塊。從個人經驗來看,集成測試很是適用於容易增加但相對簡單的應用。另外一方面,單元測試更適用於邏輯複雜的應用。儘管大多數應用都適合第一種狀況,但我將從單元測試開始更好地解釋應用層。

爲什麼選用jest

  1. 方便的異步測試
  2. snapshot功能(快照測試)
  3. 集成斷言庫,不準要引用其餘第三方庫
  4. 對React天生支持
  5. 零配置
  6. 內置代碼覆蓋率
  7. 強大的Mocks

Jest 安裝與配置

vue中直接選就能夠

在其餘的項目中,直接測試就能夠

npm install --save-dev jest

在package.json中添加

// 添加測試命令
{
  "scripts": {
    "test": "jest"
  }
}

執行命令

npm test

Jest 的測試腳本名形如.test.js,不論 Jest 是全局運行仍是經過npm test運行,它都會執行當前目錄下全部的.test.js 或 *.spec.js 文件、完成測試

Jest 的api與概念

匹配器(Matchers)

一、相等匹配

expact(2 + 2) 將返回咱們指望的結果, toBe 就是一個matcher

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

toBe 是測試具體的某一個值,若是須要測試對象,須要用到toEqual,toEqual是經過遞歸檢查對象或數組的每一個字段。

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

二、真實性匹配,好比:對象是否爲null,集合是否爲空等等

在測試中,您有時須要區分undefined、null和false,但有時但願以不一樣的方式處理這些問題,Jest幫助你明確您想要什麼。好比:

  1. toBeNull 僅當expect返回對象爲 null時
  2. toBeUndefined 僅當返回爲 undefined
  3. toBeDefined 和上面的恰好相反,對象若是有定義時
  4. toBeTruthy 匹配任何返回結果爲true的
  5. toBeFalsy 匹配任何返回結果爲false的

三、數字型匹配

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

於float類型的浮點數計算的時候,須要使用toBeCloseTo而不是 toEqual ,由於避免細微的四捨五入引發額外的問題

四、字符型匹配 toMatch 匹配規則,支持正則表達式匹配

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

五、數組類型匹配 toContain 檢查是否包含

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'beer',
];

test('the shopping list has beer on it', () => {
  expect(shoppingList).toContain('beer');
});

六、異常匹配 測試function是否會拋出特定的異常信息,能夠用 toThrow 規則

function compileAndroidCode() {
  throw new ConfigError('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(ConfigError);

  // You can also use the exact error message or a regexp
  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
  expect(compileAndroidCode).toThrow(/JDK/);
});

Asynchronous(測試異步代碼)

一、回調函數

done() 被執行則意味着callback函數被調用

function fetchData(callback) {
    setTimeout(() => {
      callback('2')
    }, 2000)
  }


  test('data is 2', done => {
    function callback(data) {
      expect(data).toBe('2');
      done();
    }
    fetchData(callback)
  })

二、promise驗證

assertions(1)表明的是在當前的測試中至少有一個斷言是被調用的,不然斷定爲失敗。

在Jest 20.0.0+ 的版本中你可使用 .resolves 匹配器在你的expect語句中,Jest將會等待一直到承諾被實現,若是承諾沒有被實現,測試將自動失敗。
若是你指望你的承諾是不被實現的,你可使用 .rejects ,它的原理和 .resolves相似

function fetchData() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('2')
      }, 2000)
    })
    
  }
    

  test('data is 2', () => {
    expect.assertions(1);
    return expect(fetchData()).resolves.toBe('2');
  })
  test('data is 2', () => { 
    expect.assertions(1);
    return expect(fetchData()).rejects.toMatch('error');
  });

三、使用 Async/Await

function fetchData(num) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if(num) {
          reject('error')
        } else {
          resolve('2')
        }
      }, 2000)
    })
    
  }


  test('data is 2', () => {
    expect.assertions(1);
    return expect(fetchData()).resolves.toBe('2');
  })


  test('the data is 2', async () => {
    expect.assertions(1);
    const data = await fetchData();
    expect(data).toBe('2');
  });
  
  test('the fetch fails with an error', async () => {
    expect.assertions(1);
    try {
      await fetchData(1);
    } catch (e) {
      expect(e).toMatch('error');
    }
  });

固然你也能夠將Async Await和 .resolves .rejects 結合起來(Jest 20.0.0+ 的版本)

test('the data is peanut butter', async () => {
  expect.assertions(1);
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  await expect(fetchData()).rejects.toMatch('error');
});

Mock Functions(模擬器)

在寫單元測試的時候有一個最重要的步驟就是Mock,咱們一般會根據接口來Mock接口的實現,好比你要測試某個class中的某個方法,而這個方法又依賴了外部的一些接口的實現,從單元測試的角度來講我只關心我測試的方法的內部邏輯,我並不關注與當前class自己依賴的實現,因此咱們一般會Mock掉依賴接口的返回,由於咱們的測試重點在於特定的方法,因此在Jest中一樣提供了Mock的功能

Jest中有兩種方式的Mock Function,一種是利用Jest提供的Mock Function建立,另一種是手動建立來覆寫自己的依賴實現。

一、 jest.fn() 方式

every

function every(array, predicate) {
    let index = -1
    const length = array == null ? 0 : array.length
  
    while (++index < length) {
      if (!predicate(array[index], index, array)) {
        return false
      }
    }
    return true
  }
  
  module.exports = every

foreach

function foreach(arr, fn) {
    for(let i = 0, len = arr.length;  i < len; i++) {
        fn(arr[i]);
    }
}

module.exports = foreach;
const foreach = require('./foreach');
const every = require('./every');

describe('mock test', () => {
    it('test foreach use mock', () => {
        
        // 經過jest.fn() 生成一個mock函數
        const fn = jest.fn();

        foreach([1, 2, 3], fn);
        // 測試mock函數被調用了3次
        expect(fn.mock.calls.length).toBe(3);
       // 測試第二次調用的函數第一個參數是3
        expect(fn.mock.calls[2][0]).toBe(3);
    })

    it('test every use mock return value', () => {
        const fn = jest.fn();
        
        // 能夠設置返回值
        fn
          .mockReturnValueOnce(true)
          .mockReturnValueOnce(false);


        const res = every([1, 2, 3, 4], fn);
        expect(fn.mock.calls.length).toBe(2);
        expect(fn.mock.calls[1][1]).toBe(1);
    })

    it('test every use mock mockImplementationOnce', () =>{
       // 快速定義mock的函數體,方便測試
        const fn = jest.fn((val, index) => {
            if(index == 2) {
                return false;
            }
            return true;
        });

        const res = every([1, 2, 3, 4], fn);
        expect(fn.mock.calls.length).toBe(3);
        expect(fn.mock.calls[1][1]).toBe(1);
    })
})

二、手動

假如個人測試文件sum2.js

function sum2(a, b) {
    if (a > 10) return a * b;
    return a + b;
}

export default sum2;
如今若是咱們要mock sum2.js 文件的話,須要在sum2.js 同級目錄下新建文件夾__mock__,
而後在此文件下新建文件同名 sum2.js, 只是單純的返回100

export default function sum2(a, b) {
    return 100;
}
測試用例mock_file.test.js


jest.mock('../src/sum2');
import sum2 from '../src/sum2';


it('test mock sum2', () => {
    // 由於此時訪問的是__mock__文件夾下的sum2.js 因此測試經過
    expect(sum2(1, 11111)).toBe(100);
})

手動mock的好處是測試和模擬分離。能夠很方便的修改測試用例。若是是複雜的mock建議使用手動新建文件方式

方便的鉤子與全局函數

  1. beforeEach(fn)每個函數以前
  2. afterEach(fn) 每個函數以後
  3. beforeAll(fn) 全部的以前
  4. afterAll(fn) 全部的以後
class Hook {

    constructor() {
        this.init();
    }

    init() {
        this.a = 1;
        this.b = 1;
    }

    sum() {
        return this.a  + this.b;
    }
}


describe('hook', () => {

    const hook = new Hook;

    // 每一個測試用例執行前都會還原數據,因此下面兩個測試能夠經過。
    beforeEach( () => {
        hook.init();
    })


    test('test hook 1', () => {
        hook.a = 2;
        hook.b = 2;
        expect(hook.sum()).toBe(4);
    })

    test('test hook 2', () => {

        expect(hook.sum()).toBe(2);// 測試經過
    })
})

describe(name, fn)

describe(name, fn)建立一個塊,在一個「測試套件」中,將幾個相關的測試組合在一塊兒

const myBeverage = {
  delicious: true,
  sour: false,
};

describe('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

這不是必需的——你能夠直接在頂層編寫測試塊。可是,若是您但願將測試組織成組,那麼這就很方便了

describe.only(name, fn)

若是你只想運行一次模塊測試的話你可使用 only

describe.only('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

describe('my other beverage', () => {
  // ... will be skipped
});

describe.skip(name, fn) describe 等價於 xdescribe

你可使用skip 跳過某一個測試

describe('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

describe.skip('my other beverage', () => {
  // ... will be skipped
});

使用跳過一般只是一種比較簡單的替代方法,若是不想運行則能夠暫時將大量的測試註釋掉。

require.requireActual(moduleName)

返回實際的模塊而不是模擬,繞過全部檢查模塊是否應該接收模擬實現。

require.requireMock(moduleName)

返回一個模擬模塊,而不是實際的模塊,繞過全部檢查模塊是否正常。

test(name, fn, timeout) 等價於 it(name, fn, timeout)

在測試文件中,您所須要的是運行測試的測試方法。例如,假設有一個函數inchesOfRain()應該是零。你的整個測試能夠是:

test('did not rain', () => {
  expect(inchesOfRain()).toBe(0);
});

第一個參數是測試名稱;第二個參數是包含測試指望的函數。第三個參數(可選)是超時(以毫秒爲單位),用於指定在停止前等待多長時間。注意:默認的超時是5秒。

注意:若是測試返回了一個promise,Jest會在測試完成以前等待promise。Jest還將等待,若是你爲測試函數提供一個參數,一般稱爲done。當你想要測試回調時,這將很是方便。請參見如何在此測試異步代碼。

test.only(name, fn, timeout)等同於 it.only(name, fn, timeout) or fit(name, fn, timeout)

test.skip(name, fn)等同於it.skip(name, fn) or xit(name, fn) or xtest(name, fn)

當您維護一個大型的代碼庫時,您可能有時會發現因爲某種緣由而臨時中斷的測試。

若是您想跳過這個測試,可是您不想僅僅刪除這個代碼,您可使用skip指定一些測試來跳過。

test('it is raining', () => {
  expect(inchesOfRain()).toBeGreaterThan(0);
});

test.skip('it is not snowing', () => {
  expect(inchesOfSnow()).toBe(0);
});

只有「it is raining」測試運行,由於另外一個測試運行test . skip。 您能夠簡單地對測試進行註釋,可是使用skip會更好一些,由於它將保持縮進和語法突出。

測試覆蓋率

Jest 內置了測試覆蓋率工具istanbul,要開啓,能夠直接在命令中添加 --coverage 參數,或者在 package.json 文件進行更詳細的配置。

搭配React和其它框架的使用 快照功能

快照測試第一次運行的時候會將被測試ui組件在不一樣狀況下的渲染結果保存一份快照文件。後面每次再運行快照測試時,都會和第一次的比較。

import React from 'react';

export default class RC extends React.Component {

    render() {
        return (
            <div>我是react組件 </div>
        )
    }
}
import React from 'react';
import renderer from 'react-test-renderer';

import RC from '../src/react-comp';

test('react-comp snapshot test', () => {
    const component = renderer.create(<RC />);
    //
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
})

test('react-comp snapshot test2', () => {
    const component = renderer.create(<RC />);
    
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
})

執行測試命令,會在test目錄下生成一個__snapshots__目錄,在此目錄下會與一個文件叫snapshot.test.js.snap的快照文件

遷移

使用

npm install -g jest-codemods


而後jest-codemods

enzyme API

shallow() 渲染函數只渲染咱們專門測試的組件, 它不會渲染子元素。相反, 用mount()
一、使用 shallow

mport { shallow } from 'enzyme';

const wrapper = shallow(<MyComponent />);

咱們剛剛能夠看到這個測試裏用到shallow函數,它支持對DOM的進行結構和事件的響應,若是你對jQuery比較熟悉的話,那麼你對它的語法也不會陌生。好比咱們測試裏用到的find方法,你們常常用它來尋找一些DOM數組。

簡單羅列下它所支持的方法:

  • find(selector) 查找選擇器下的DOM 元素,返回一個數組。
  • contains(node) 肯定是否包含該節點或者一些節點 ,返回true 或者 false
  • is(selector) 判斷改節點是否可以匹配選擇器的節點 ,返回true 或者 false
  • hasClass(className) 判斷是否包含這個類,返回true 或者 false
  • prop[key] 返回組件上某個屬性的值
  • setState(props) 設置組件狀態
  • simulate(event[,mock]) 模擬一個節點上的事件

二、徹底DOM渲染

import { mount } from 'enzyme';

const wrapper = mount(<MyComponent />);

徹底DOM渲染主要用於與DOM API進行交互以及須要完整生命週期的組件測試(i.e componentDidMoun)。徹底DOM渲染須要DOM 的 API 在全局做用域內。並且須要其運行在近似瀏覽器的環境裏。若是你不想在瀏覽器裏跑這些測試的話,強烈建議你使用mount,一個依賴於jsdom的類庫,幾乎等同於沒有瀏覽器外殼的瀏覽器。它也支持了不少方法

三、靜態渲染

靜態渲染,enzyme還提供了靜態渲染,將組件渲染成html,用於咱們分析html的結構。render相比前兩種用法, 主要是在於更換了類庫 Cheerio ,並且做者也相信在處理解析上會更好點。

import { render } from 'enzyme';

const wrapper = render(<MyComponent />);

使用說明

若是咱們在開發過程當中就進行了測試(直接採用 TDD 開發模式、或者針對既有的模塊寫用例),

會有以下的好處:

  • 保障代碼質量和功能的實現的完整度
  • 提高開發效率,在開發過程當中進行測試能讓咱們提早發現 bug ,此時進行問題定位和修復的速度天然比開發完再被叫去修 bug 要快許多
  • 便於項目維護,後續任何代碼更新也必須跑通測試用例,即便進行重構或開發人員發生變化也能保障預期功能的實現

固然,凡事都有兩面性,好處雖然明顯,卻並非全部的項目都值得引入測試框架,畢竟維護測試用例也是須要成本的。對於一些需求頻繁變動、複用性較低的內容,好比活動頁面,讓開發專門抽出人力來寫測試用例確實得不償失。而那些適合引入測試場景大概有這麼幾個:

  • 須要長期維護的項目。它們須要測試來保障代碼可維護性、功能的穩定性較爲穩定的項目、或項目中
  • 較爲穩定的部分。給它們寫測試用例,維護成本低
  • 被屢次複用的部分,好比一些通用組件和庫函數。

由於多處複用,更要保障質量

相關文章
相關標籤/搜索