JS單元測(cè)試

1. 環(huán)境準(zhǔn)備

初始項(xiàng)目

cnpm init
//或者
yarn init

安裝jest

cnpm install --save-dev jest
// 或者
yarn add --dev jest

下載和配置babel
(如果不使用ES6語(yǔ)法,則可以跳過(guò)此步驟)

cnpm install @babel/plugin-transform-modules-commonjs --save-dev

新建一個(gè).babelrc文件,寫(xiě)入如下內(nèi)容,或者直接在package.json中配置。

{
  "env": {
    "test": {
      "plugins": ["@babel/plugin-transform-modules-commonjs"]
    }
  }
}

2. 基礎(chǔ)函數(shù)測(cè)試

① toBe
精確的比較,本質(zhì)是用Object.is來(lái)比較,取反使用not.toBe

export function sum(a, b) {
     return a + b;
}
test('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
});

② toEqual

可以用來(lái)比較對(duì)象的值是否相同

export function returnObj(age) {
     return {
          name: "小哈",
          age: age + 1
     }
}
test('compare object', () => {
    let result = returnObj(10);
    expect(result).toEqual({name: "小哈", age: 11 });
    expect(result).toBe({name: "小哈", age: 11 });
});

③ 函數(shù)拋出異常的測(cè)試

// 3. 拋出異常
export function throwError() {
     throw new Error("i am sorry,there has something wrong!")
}
test('test error funciton', () => {
    expect(throwError).toThrow(Error);
})

④ 其他可以根據(jù)特定的情況選擇方法

test('sth special in need', () => {
    let data1 = null, data2, data3 = 0.1 + 0.2;
    expect(data1).toBeNull;
    expect(data2).toBeDefined;
    expect(data3).toBeCloseTo(0.3);
    expect(1 === 1).toBeTruthy();
    expect(0 === 1).toBeFalsy();
})

浮點(diǎn)數(shù)的比較,由于精度的差異,則可以使用toBeCloseTo來(lái)比較
⑤專門測(cè)試,跳過(guò)其他測(cè)試

用于重點(diǎn)排查一些錯(cuò)誤,可以使用only,例:

test.only('...', () => {
  expect(true).toBe(false);
});

3. 異步函數(shù)測(cè)試

在js中最常見(jiàn)的就是請(qǐng)求異步函數(shù)了,業(yè)務(wù)中基本都是增刪改查,CURD;如果在測(cè)試中直接使用回調(diào)函數(shù),但是jest測(cè)試執(zhí)行到末尾就會(huì)結(jié)束,不會(huì)等待回調(diào)完成;

舉個(gè)例子:

異步函數(shù)定義

export function fetchData(callback) {
     try {
          setTimeout(() => {
               callback("success");
          }, 2000)
     }catch(err) {
          callback("failed");
     }
}

①直接調(diào)用測(cè)試:

import { fetchData, Asynchronous } from './functionModules';
test("call the async function directly", () => {
  function callback(info) {
      expect(info).toBe("success");
      console.log("I'm callback function, I have performed")
  }
  fetchData(callback);
})

可以發(fā)現(xiàn)打印沒(méi)有被執(zhí)行,直接測(cè)試成功,等不及callback執(zhí)行,直接跳過(guò)了。
②對(duì)①進(jìn)行改進(jìn),使用done

test("call the async function with done", done => {
  function callback(info) {
    try{
      expect(info).toBe("success");
      done();
    }catch(err) {
      done(err);
    }
  }
  fetchData(callback);
})

jest測(cè)試會(huì)等待done被執(zhí)行,如果沒(méi)有任何done被執(zhí)行,則會(huì)發(fā)生超時(shí)錯(cuò)誤,所以我們要使用try...catch來(lái)捕獲異常,所以此處會(huì)等待expect被執(zhí)行。
③使用返回一個(gè)promise的方法
注意點(diǎn)就是一定要return!!!
調(diào)用的函數(shù):

/**
 * 返回異步函數(shù)
 */
export function Asynchronous() {
     return new Promise((resolve, reject) => {
          try {
               setTimeout(() => {
                    resolve("success")
               }, 2000)
          }catch(err) {
               reject("failed")
          }
     })
}
/**
 * 返回異步函數(shù),發(fā)生錯(cuò)誤情況reject
 */
export function AsynchronousFailed() {
     return new Promise((resolve, reject) => {
          try {
               throw Error("failed");
          }catch(err) {
               reject("failed")
          }
     })
}

jest測(cè)試,以下都會(huì)通過(guò)

test("use promise when resolve", () => {
  return Asynchronous().then(res => {
    expect(res).toBe("success")
  })
})
test("use promise when resolve", () => {
  return AsynchronousFailed().catch(err => {
    expect.assertions(1);
    expect(err).toBe("failed")
  })
})

可以發(fā)現(xiàn)此處用到了expect.assertions,顧名思義,此處就是聲明斷言的次數(shù),經(jīng)常用于異步的代碼中,為了確保斷言的回調(diào)能夠按照預(yù)期進(jìn)行。
expect.assertions
④直接測(cè)試resolve/reject
同樣的需要return返回才會(huì)等待異步的執(zhí)行,注意是resolve + s, reject + s

test("judge resolve directly", () => {
  return expect(Asynchronous()).resolves.toBe("success")
})

test("judge reject directly", () => {
  return expect(AsynchronousFailed()).rejects.toBe("failed")
})

如果使用async,await則可以改寫(xiě)成:

test("judge reject directly when use async", async () => {
  await expect(AsynchronousFailed()).rejects.toBe("failed")
})

test("judge reject directly when use async", async () => {
  await expect(AsynchronousFailed()).rejects.toBe("failed")
})

4. 生命周期

  • 當(dāng)我們需要在每個(gè)單元測(cè)試前都執(zhí)行某些操作,我們可以使用beforeEach,相應(yīng)的也有每次執(zhí)行結(jié)束之后的方法:afterEach;
  • 如果我們需要在每次測(cè)試的開(kāi)頭,有且進(jìn)執(zhí)行一次,則可以使用beforeAll, 對(duì)應(yīng)結(jié)束之后的方法:afterAll。
  • 如果是對(duì)特定的單元測(cè)試,特定時(shí)候執(zhí)行某些操作,則可以使用describe塊包裹起來(lái),這樣在塊中定義的方法只有對(duì)內(nèi)部起作用。
    舉個(gè)例子:
import { sum } from './functionModules';

beforeEach(() => console.log("beforeEach-1"));
afterEach(() => console.log("afterEach-1"));
beforeAll(() => console.log("beforeAll-1"));
afterAll(() => console.log("afterAll-1"));
test("test1", () => console.log("test1"));
console.log("outer")
describe("test2", () => {
  beforeEach(() => console.log("beforeEach-2"));
  afterEach(() => console.log("afterEach-2"));
  beforeAll(() => console.log("beforeAll-2"));
  afterAll(() => console.log("afterAll-2"));
  test("child test", () => {
    console.log("test2")
    expect(sum(1, 2)).toBe(3)
  })
  console.log("inner")
})
test("test3", () => console.log("test3"));

優(yōu)先級(jí)規(guī)則:

beforeAll ->
beforeEach ->  測(cè)試內(nèi)容1 -> afterEach
beforeEach ->  測(cè)試內(nèi)容2 -> afterEach
// 如果有describe, 繼續(xù)按照此規(guī)則
afterAll

注意describe中的test執(zhí)行時(shí),全局的beforeEach也會(huì)被執(zhí)行,jest會(huì)在真正測(cè)試開(kāi)始之前執(zhí)行所有其他代碼,默認(rèn)按測(cè)試順序執(zhí)行,執(zhí)行結(jié)果如下:

outer
inner
beforeAll-1
// 執(zhí)行外層第一個(gè)test
beforeEach-1
test1
afterEach-1
// 執(zhí)行describe塊
beforeAll-2
beforeEach-1
beforeEach-2
test2
afterEach-2
afterEach-1
afterAll-2
// 執(zhí)行外層第二個(gè)test
beforeEach-1
test3
afterEach-1
afterAll-1

5. mock函數(shù)

我們可以擦除函數(shù)的實(shí)際實(shí)現(xiàn),使用mock函數(shù)

  • 捕獲對(duì)函數(shù)的調(diào)用
  • 改變內(nèi)部結(jié)構(gòu)
  • 在使用 new 實(shí)例化時(shí)捕獲構(gòu)造函數(shù)的實(shí)例
  • 允許測(cè)試時(shí)配置返回值

下面介紹幾種mock函數(shù)中常用的方法和屬性:
① jest.fn()
使用mock函數(shù),我們可以根據(jù)特定的方法或者mock屬性獲取返回值,調(diào)用參數(shù),調(diào)用情況等等。

test("mock function", () =>{
  let fn = jest.fn();
  // 使用mockReturnValueOnce可以鏈?zhǔn)皆O(shè)置返回值
  fn.mockReturnValueOnce('a').mockReturnValueOnce('b');
  let res1 = fn(1, 2), res2 = fn(3, 4);
  
  // 獲取返回值
  expect(res1).toBe('a');
  expect(res2).toBe('b');
  expect(fn.mock.results[0].value).toBe('a');
  expect(fn.mock.results[1].value).toBe('b');

  // 是否被調(diào)用了
  expect(fn).toBeCalled();

  // 判斷調(diào)用的次數(shù)
  expect(fn).toBeCalledTimes(2);
  expect(fn.mock.calls.length).toBe(2);

  // 判斷調(diào)用的參數(shù)
  expect(fn).toHaveBeenCalledWith(1, 2);
  expect(fn).toHaveBeenCalledWith(3, 4);

  // 第一次調(diào)用的第一個(gè)參數(shù)值
  expect(fn.mock.calls[0][0]).toBe(1);
}, [])

使用mock promise函數(shù)

test('test jest.fn() by Promise', async () => {
  let fn = jest.fn().mockResolvedValue('a');
  let res = await fn();
  expect(res).toBe('a');
  expect(Object.prototype.toString.call(fn())).toBe("[object Promise]");
})

測(cè)試axios請(qǐng)求的API:

/**
 * axios 請(qǐng)求函數(shù)
 * @param { function } callback
 */
export async function getToDoList(callback) {
     return axios.get("https://jsonplaceholder.typicode.com/todos").then(res => {
          callback(res.data)
          return res;
     })
}

對(duì)應(yīng)的jest測(cè)試:

test("test axios", async () => {
  expect.assertions(1);
  let fn = jest.fn();
  await getToDoList(fn); 
  console.log(fn.mock.calls[0][0].data) // 打印出結(jié)果
  expect(fn).toBeCalledTimes(1); // 判斷fn是否被調(diào)用了一次
})

還可以不調(diào)用具體的API,通過(guò)mock返回假數(shù)據(jù),即:fake response

import axios from 'axios';
import { getToDoList } from './functionModules';
jest.mock('axios');

test("pretend to call axios api", async () => {
  let myResult = { data: [{id: 1, title: "get up early", complete: false}] };
  axios.get.mockResolvedValue(myResult);
  //axios.get.mockImplementation(() => Promise.resolve(myResult))
  return getToDoList(() => {}).then(data => expect(data).toEqual(myResult));
})

注意此處要和上面的代碼分開(kāi)兩個(gè)文件,否則會(huì)影響真實(shí)API測(cè)試的結(jié)果。當(dāng)我們需要定義一個(gè)模塊函數(shù)的默認(rèn)實(shí)現(xiàn)時(shí),我們可以使用mockImplementation來(lái)模擬數(shù)據(jù),如上,使用注釋的語(yǔ)句,測(cè)試也能通過(guò)。
以下代碼來(lái)自官網(wǎng)

// foo.js
module.exports = function () {
  // some implementation;
};
// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();

同樣可以根據(jù)調(diào)用順序設(shè)置返回值,當(dāng)mockImplementationOnce定義的都執(zhí)行完成了,則會(huì)執(zhí)行jest.fn中定義的。

const myMockFn = jest
  .fn()
  .mockImplementationOnce(cb => cb(null, true))
  .mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

參考資料

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

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