React躬行記(14)——測試框架

  測試不只能夠發現和預防問題,還能下降風險、減小企業損失。在React中,涌現了多種測試框架,本節會對其中的Jest和Enzyme作詳細的講解。html

1、Jest

  Jest是由Facebook開源的一個測試框架,可無縫兼容React項目,專一簡單,推崇零配置,開箱即用的宗旨,用於邏輯和組件的單元測試。它的語法和斷言與Jasmine相似,而且還集成了快照測試、Mock、覆蓋率報告等功能,支持多進程並行運行測試,在內部使用JSDOM操做DOM,JSDOM是一種模擬的DOM環境,其行爲相似於常規瀏覽器,可用來與用戶交互、在節點上派發事件等。react

1)運行ios

  爲了便於運行Jest,本文使用Create React App建立項目,命令以下所示。git

npx create-react-app my-app

  只要把測試文件放置在__tests__目錄內,或將它們的名稱添加.test.js或.spec.js後綴,並保存在項目的src目錄中的任何深度,就能被Jest檢測到。當運行下面的命令時,可獲得相關的測試結果。github

npm test

  默認狀況下,Jest每次只運行與本次更改的文件相關的測試用例。npm

2)建立測試json

  若是要建立測試用例(Test Case),那麼須要使用test()或it()函數,其第一個參數是測試名稱,第二個參數是包含測試代碼的回調函數,以下所示。axios

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

  expect()函數用於斷言,它能接收一個實際值,並將其做爲結果與匹配器中的指望值作比較。若是匹配失敗,那麼就會在控制檯輸出相應的錯誤提示。api

  describe()函數可將測試用例進行邏輯分組,其第一個參數可定義分組的名稱,以下所示。數組

describe("my test case", () => {
  test("one plus one is two", () => {
    expect(1 + 1).toBe(2);
  });
  test("two plus two is four", () => {
    expect(2 + 2).toBe(4);
  });
});

3)匹配器

  經過匹配器(Matcher)能夠各類方式來測試代碼,例如以前示例中的toBe()就是一個匹配器,它使用Object.is()來測試精確匹配,若是要檢查對象是否相等,可改用toEqual(),以下所示。

test("object assignment", () => {
  const data = { name: "strick" };
  data["age"] = 28;
  expect(data).toEqual({ name: "strick", age: 28 });
});

  其它經常使用的匹配器還有區分undefined、null和布爾值、比較數字、匹配字符串、檢查數組或可迭代對象是否包含某個特定項、測試拋出的錯誤等功能。

  全部的匹配器均可以經過.not取反,例如驗證toBeUndefined()不能匹配null,以下所示。

test("null is not undefined", () => {
  expect(null).not.toBeUndefined();
});

4)異步測試

  Jest提供了多種方式來測試異步代碼,包括回調函數、Promise和Async/Await,接下來會逐個講解用法。

  (1)默認狀況下,Jest測試一旦執行到末尾就會完成,例若有一個check()函數(以下所示),它能接收一個回調函數,一旦check()執行結束,此測試就會在沒有執行回調函數前結束。

function check(func) {
  const success = true;
  func(success);
}
test("the data is truth", () => {
  function callback(data) {
    expect(data).toBeTruthy();
  }
  check(callback);
});

  若要解決此問題,可爲test()的回調函數傳遞一個名爲done的函數參數,Jest會等done()回調函數執行完後,再結束測試,以下所示。

test("the data is truth", done => {
  function callback(data) {
    expect(data).toBeTruthy();
    done();
  }
  check(callback);
});

  (2)當異步代碼返回Promise對象時,Jest會等待其狀態的變化。若是狀態變爲已完成,那麼得使用then()方法;若是狀態變爲已拒絕,那麼得使用catch()方法,以下所示。

//狀態爲已完成
function checkResolve() {
  return new Promise((resolve, reject) => {
    resolve(true);
  });
}
test("the data is truth", () => {
  return checkResolve().then(data => {
    expect(data).toBeTruthy();
  });
});
//狀態爲已拒絕
function checkReject() {
  return new Promise((resolve, reject) => {
    reject(false);
  });
}
test("the data is falsity", () => {
  return checkReject().catch(data => {
    expect(data).toBeFalsy();
  });
});

  注意,要將Promise對象做爲test()的回調函數的返回值,以避免測試提早完成,致使沒有進行方法鏈中的斷言。

  在expect語句中也可使用.resolves或.rejects兩種匹配器來處理Promise的兩種狀態,以下所示,語法更爲簡潔。

test("the data is truth", () => {
  expect(checkResolve()).resolves.toBeTruthy();
});
test("the data is falsity", () => {
  expect(checkReject()).rejects.toBeFalsy();
});

  (3)在測試中使用async和await兩個關鍵字,也能夠匹配Promise對象,例如斷言checkResolve()的處理結果,以下所示。

test("the data is truth", async () => {
  const data = await checkResolve();
  expect(data).toBeTruthy();
});

  它們也能用來測試已拒絕狀態的Promise,以下所示,其中assertions()用於驗證在測試中是否執行了指定數量的斷言。

function checkError() {
  return new Promise((resolve, reject) => {
    reject();
  }).catch(() => {
    throw "error";
  });
}
test("the check fails with an error", async () => {
  expect.assertions(1);
  try {
    await checkError();
  } catch (e) {
    expect(e).toMatch("error");
  }
});

  aysnc和awiat還能夠與.resolves或.rejects結合使用,以下所示。

test("the data is truth", async () => {
  await expect(checkResolve()).resolves.toBeTruthy();
});
test("the check fails with an error", async () => {
  await expect(checkError()).rejects.toMatch("error");
});

5)輔助函數

  有時候,在運行測試前須要作些準備工做,而在運行測試以後又須要作些整理工做,Jest提供了四個相關的輔助函數來處理這兩類工做,以下所列。

  (1)beforeAll()和afterAll()會在全部測試用例以前和以後執行一次。

  (2)beforeEach()和afterEach()會在每一個測試用例以前和以後執行,而且能夠像異步測試那樣處理異步代碼。

  假設在四個輔助函數中輸出各自的函數名稱,而且有兩個測試用例,以下代碼所示。

beforeAll(() => {
  console.log("beforeAll");
});
afterAll(() => {
  console.log("afterAll");
});
beforeEach(() => {
  console.log("beforeEach");
});
afterEach(() => {
  console.log("afterEach");
});

test("first", () => {
  expect(2).toBeGreaterThan(1);
});
test("second", () => {
  expect(2).toBeLessThan(3);
});

  每次運行測試,在控制檯將依次打印出「beforeAll」,兩對「beforeEach」和「afterEach」,「afterAll」。

  當經過describe()對測試用例進行分組時(以下所示),外部的beforeEach()和afterEach()會優先執行。

describe("scoped", () => {
  beforeEach(() => console.log("inner beforeEach"));
  afterEach(() => console.log("inner afterEach"));
  test("third", () => {
    expect([1, 2]).toContain(1);
  });
});

6)Mock

  Jest內置了Mock函數,可用於擦除函數的實際實現來測試代碼之間的鏈接,捕獲函數的調用和參數、配置其返回值等。

  假設要測試一個自定義的forEach()函數的內部實現,那麼可使用jest.fn()建立一個Mock函數,而後經過檢查它的mock屬性來確保回調函數是否在按預期調用,以下所示。

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}
test("forEach", () => {
  const mockFunc = jest.fn(x => 42 + x);
  forEach([0, 1], mockFunc);
  expect(mockFunc.mock.calls.length).toBe(2);            //此Mock函數被調用了兩次
  expect(mockFunc.mock.calls[0][0]).toBe(0);             //第一次調用函數時的第一個參數是0
  expect(mockFunc.mock.calls[1][0]).toBe(1);             //第二次調用函數時的第一個參數是1
  expect(mockFunc.mock.results[0].value).toBe(42);       //第一次函數調用的返回值是42
});

  每一個Mock函數都會包含一個特殊的mock屬性,記錄了函數如何被調用、調用時的返回值等信息,經過該屬性還能追蹤每次調用時的this的值。若是要用Mock函數注入返回值,那麼能夠像下面這樣鏈式的添加,首次調用返回10,第二次調用返回「x」,接下來的調用都返回true。其中mockName()方法可爲Mock函數命名,該名稱將在輸出的日誌中顯示,可替換掉默認的「jest.fn()」。

const myMock = jest.fn().mockName("returnValue");
myMock
  .mockReturnValueOnce(10)
  .mockReturnValueOnce("x")
  .mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());     //10, 'x', true, true

  Mock函數還能夠模擬模塊,例如攔截axios請求獲得的數據,以下代碼所示,爲.get提供了一個mockResolvedValue()方法,它會返回用於測試的假數據。

import axios from "axios";
jest.mock("axios");
class Users {
  static all() {
    return axios.get("./users.json").then(resp => resp.data);
  }
}
test("should fetch users", () => {
  const users = [{ name: "strick" }];
  const resp = { data: users };
  axios.get.mockResolvedValue(resp);
  return Users.all().then(data => expect(data).toEqual(users));
});

  原生的定時器函數測試起來並不方便,經過jest.useFakeTimers()能夠模擬定時器函數,以下所示。

function timerGame() {
  setTimeout(() => {
    console.log("start");
  }, 1000);
}
jest.useFakeTimers();
test("setTimeout", () => {
  timerGame();
  expect(setTimeout).toHaveBeenCalledTimes(1);                               //調用了1次
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);        //1秒後執行回調
});

  Jest模擬出的定時器函數還有快進到正確的時間點、執行當前正在等待的定時器等功能。

7)快照測試

  Jest提供的快照測試(Spapshot Testing)是一種高效的UI測試,它會將React組件序列化成純文本(即快照)並保存在硬盤中,每次測試就把當前生成的快照與保存的快照進行對比,接下來用一個例子來介紹快照測試的用法。

  首先建立一個Link組件,它會渲染出一條包含onMouseEnter事件的連接,當鼠標移動到這條連接時,會改變它的class屬性。

import React from "react";
const STATUS = {
  HOVERED: "hovered",
  NORMAL: "normal"
};
export default class Link extends React.Component {
  constructor(props) {
    super(props);
    this._onMouseEnter = this._onMouseEnter.bind(this);
    this.state = {
      class: STATUS.NORMAL
    };
  }
  _onMouseEnter() {
    this.setState({ class: STATUS.HOVERED });
  }
  render() {
    return (
      <a
        href="#"
        className={this.state.class}
        onMouseEnter={this._onMouseEnter}
      >
        {this.props.children}
      </a>
    );
  }
}

  而後建立測試文件spapshot.test.js,在其內部,除了要引入Link組件以外,還得引入react-test-renderer,它不依賴瀏覽器和JSDOM,可將React組件渲染成JavaScript對象(即快照)。

import React from "react";
import Link from "./Link";
import renderer from "react-test-renderer";

test("Link changes the class when hovered", () => {
    const component = renderer.create(<Link>Strick</Link>);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();

    tree.props.onMouseEnter();         //觸發事件
    tree = component.toJSON();         //從新渲染
    expect(tree).toMatchSnapshot();
});

  在第一次運行測試時,會自動建立__snapshots__目錄,放置對應的快照文件spapshot.test.js.snap,其內容以下所示,包含兩張快照,第二張是觸發onMouseEnter事件後生成的。

exports[`Link changes the class when hovered 1`] = `
<a
  className="normal"
  href="#"
  onMouseEnter={[Function]}
>
  Strick
</a>
`;

exports[`Link changes the class when hovered 2`] = `
<a
  className="hovered"
  href="#"
  onMouseEnter={[Function]}
>
  Strick
</a>
`;

  若是要刷新保存的快照,除了手動刪除以外,還能夠經過jest -u命令實現。

2、Enzyme

  Enzyme是一款用於React組件的測試框架,可處理渲染出的DOM結構,開放的API相似於jQuery的語法,提供了三種不一樣的方式來測試組件:淺層渲染(Shallow Rendering)、徹底渲染(Full Rendering)和靜態渲染(Static Rendering)。從Enzyme 3開始,在安裝Enzyme的同時,還須要安裝與React版本相對應的適配器,命令以下所示。

npm install --save enzyme enzyme-adapter-react-16

1)淺層渲染

  獨立於DOM的淺層渲染只會渲染React組件的第一層,它會忽略子組件的行爲,也就不必渲染子組件了,這提供了更好的隔離性。不過淺層渲染也有它侷限性,即不支持Refs。

  以上一節中的Link組件爲例,在進行Enzyme以前,須要先經過configure()函數配置適配器,而後才能經過shallow()函數淺渲染Link組件,以下所示。

import React from "react";
import { shallow, configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Link from "../component/Form/Link";

configure({ adapter: new Adapter() });
test("Link changes the class after mouseenter", () => {
  const wrapper = shallow(<Link>Strick</Link>),
    a = wrapper.find("a");
  expect(wrapper.text()).toEqual("Strick");
  a.simulate("mouseenter");                         //觸發事件
  expect(a.prop("className")).toEqual("normal");       //匹配樣式
});

  wrapper是一個虛擬的DOM對象,它包含多個操做DOM的方法,例如find()可根據選擇器找到指定的節點,simulate()可觸發當前節點的事件。

2)徹底渲染

  mount()函數會徹底渲染接收的組件,即它的子組件也會被渲染。徹底渲染依賴JSDOM,當多個測試處理同一個DOM時,可能會相互影響,所以在測試結束後須要使用unmount()方法卸載組件。

3)靜態渲染

  render()函數會靜態渲染組件,也就是將它渲染成HTML字符串,再經過Cheerio庫解析該HTML結構。Cheerio相似於JSDOM,但更輕量,可像jQuery那樣操做字符串。

相關文章
相關標籤/搜索