Testing library 101 (二)

書接上文戈钢,上篇說到了 React Testing library 的安裝和最基本用法。本篇繼續(xù)深挖一些較復(fù)雜的場(chǎng)景。

mock 測(cè)試

開始 RTL 測(cè)試前,我們稍微回顧一下 Jest 的 Mock 測(cè)試。

所謂 mock 測(cè)試就是在測(cè)試過程中钉寝,對(duì)于某些不容易構(gòu)造或者不容易獲取的對(duì)象,用一個(gè)虛擬的對(duì)象代替具體實(shí)現(xiàn)闸迷,以便繼續(xù)測(cè)試進(jìn)度的方式嵌纲。

其中最常用的 mock 方式有兩種:

  1. 創(chuàng)建一個(gè) mock 函數(shù),注入到目標(biāo)用例里腥沽, 如 jest.fn
  2. 設(shè)計(jì)一個(gè)手工的 mock 對(duì)象逮走,來覆蓋依賴模塊,如 jest.mock

jest.fn()

先來說 mock 函數(shù)注入今阳。我們寫一個(gè)最最基礎(chǔ)的 repeatTen 函數(shù)师溅,功能就是調(diào)用10 次其他函數(shù)。

function repeatTen(fn) {
  for (let i = 0; i < 10; i++) {
    fn();
  }
}

repeatTen(() => console.log("Onion"));

repeatTen 函數(shù)的特點(diǎn)就是:它根本不關(guān)心 fn 的具體實(shí)現(xiàn)盾舌,只要保證 fn 一次性能跑到10趟就行墓臭。這類單元測(cè)試就非常適合使用 mock 函數(shù)了。如下所示妖谴,我們以參數(shù)的形式窿锉,為 repeatTen 傳入一個(gè) mock 函數(shù)(onChang),然后檢查 mock 函數(shù)的調(diào)用狀態(tài)膝舅,判定函數(shù)是否如期運(yùn)行就行了嗡载。

it("Count the calls of the onChange", () => {
  const onChange = jest.fn();
  repeatTen(onChange);
  // assert that it is called 10 times
  expect(onChange).toHaveBeenCalledTimes(10);
});

jest.mock()

上文 repeatTen 函數(shù)的實(shí)現(xiàn)比較單純,現(xiàn)實(shí)世界則復(fù)雜很多仍稀;有些函數(shù)的實(shí)現(xiàn)會(huì)依賴于三方庫鼻疮。在測(cè)試環(huán)境里你很難以傳入一個(gè) mock 函數(shù)的形式來模擬真實(shí)的運(yùn)行狀態(tài)。比如下面這個(gè)函數(shù)琳轿,通過 axios 調(diào)用 API 來返回?cái)?shù)據(jù)判沟,大家想想該怎么寫 test case。

const loadStories = () => axios.get("/stories");

我們要測(cè)試 loadStories 但又不大可能調(diào)用真實(shí)的 API崭篡,就只能對(duì) axios 的內(nèi)部模塊動(dòng)點(diǎn)小腦筋了——用 jest.mock("axios") 自動(dòng)模擬 axios 模塊挪哄。

Jest 提供了一個(gè)很有意思的依賴覆蓋方法——jest.mock("axios"),它會(huì)給對(duì)象模塊——axios——的.get方法提供一個(gè) mockResolvedValue琉闪。通俗來說迹炼,就是讓axios.get("/stories") 返回一個(gè)假的 response;而這個(gè) response 的數(shù)據(jù)是我們事先準(zhǔn)備好的颠毙。

看一下寫 mock 依賴測(cè)試的主要流程:

  1. 利用 jest.mock(...) 覆蓋用例函數(shù)內(nèi)部 import 的依賴
  2. 為用例函數(shù)內(nèi)部使用的某個(gè)方法構(gòu)造一個(gè) mock 返回
  3. 斷言用例函數(shù)的返回結(jié)果
import axios from "axios";

// step1: override a module
jest.mock("axios");

const mockStories = [
  { objectID: "1", title: "Hello" },
  { objectID: "2", title: "React" },
];

it("load stories by axios", async () => {
  // step2: make a fake response
  const response = { data: mockStories };
  axios.get.mockResolvedValue(response);

  // step3: assert the data
  const { data } = await loadStories();
  expect(data).toEqual(mockStories);
});

callback 測(cè)試

OK斯入,兜兜轉(zhuǎn)轉(zhuǎn)說了很長(zhǎng)篇幅的 Jest 方法。我們還是回到 React Testing library(以下簡(jiǎn)稱RTL)蛀蜜。

如果大家看懂了 Jest.fn 章節(jié)的內(nèi)容刻两,RTL 的 callback 測(cè)試就一目了然了——就是來測(cè)試 onChange 事件的。我們寫一個(gè)簡(jiǎn)單的搜索條滴某,通過 onChange 事件返給父組件輸入的內(nèi)容磅摹。

// CallbackSearch.js
export function CallbackSearch({ onChange }) {
  return (
    <div>
      <label htmlFor="search">Search:</label>
      <input id="search" type="text" onChange={onChange} />
    </div>
  );
}

該組件的測(cè)試怎么寫呢?很簡(jiǎn)單霎奢,反正只有一個(gè) onChange 參數(shù)户誓,我們只要測(cè)試它的調(diào)用狀態(tài)就行了。

import userEvent from "@testing-library/user-event";

describe("Search", () => {
  it("paste counts", () => {
    const onChange = jest.fn();
    render(<CallbackSearch onChange={onChange} />);

    const $e = screen.getByRole("textbox");
    userEvent.paste($e, "Onion");

    expect(onChange).toHaveBeenCalledTimes(1);
  });
});

這次我們使用的是 userEvent 的paste方法幕侠,輸入框內(nèi)黏貼一段文本帝美,自然只觸發(fā)一次事件,所以斷言 onChange 被調(diào)用了 1 次即可晤硕。假如你用的是userEvent.type悼潭,那就是每輸入一個(gè)字符都會(huì)回調(diào)一次,就要斷言 onChange 的掉用次數(shù)為輸入字符串的長(zhǎng)度了窗骑。

異步加載測(cè)試

上一章講了 jest.fn 的案例女责,我們?cè)僬f說 jest.mock 的案例。

React Hook

現(xiàn)在不是流行寫 Hook 嗎创译?我們就寫個(gè) loadStories 的加強(qiáng)版 Hook抵知,主要功能還是老樣子,利用 axios 遠(yuǎn)程調(diào)用 API软族;成功了就把數(shù)據(jù)寫到 stories 這個(gè) state 里刷喜,失敗了就把錯(cuò)誤寫到 error 這個(gè) state 里。

// userStory.js
import axios from "axios";
import { useState, useCallback } from "react";

export function useStory() {
  const [stories, setStories] = useState([]);
  const [error, setError] = useState(null);

  const handleFetch = useCallback(() => {
    const loadStories = async () => {
      try {
        const { data } = await axios.get("/stories");
        setStories(data);
      } catch (error) {
        setError(error);
      }
    };
    loadStories();
  }, []);

  return { error, stories, handleFetch };
}

給 React Hook 寫單元測(cè)試需要額外安裝一個(gè)依賴立砸,@testing-library/react-hooks掖疮;主要是用了它的一個(gè)方法 readerHook 來模擬 react 組件的場(chǎng)景。

我們看一下怎么寫這個(gè) Hook 的 test case颗祝,套路是相似的:

  1. 利用 jest.mock 覆蓋 axios 模塊
  2. 偽造一個(gè) axios.get 的 response
  3. 斷言 stories 的初試狀態(tài)是空數(shù)組
  4. 調(diào)用異步方法加載 stories
  5. 完成異步調(diào)用后浊闪,斷言 stories 的最新狀態(tài)
import { renderHook } from "@testing-library/react-hooks";
import axios from "axios";
import { useStory } from "./useStory";

// Step 1: override a module
jest.mock("axios");

const mockStory = [
  { objectID: "1", title: "Hello" },
  { objectID: "2", title: "React" },
];

it("load stories and succeed", async () => {
  // Step 2: fake a response
  const response = { data: mockStory };
  axios.get.mockResolvedValue(response);

  const { result, waitForNextUpdate } = renderHook(() => useStory());

  // Step 3: test initial state
  expect(result.current.stories).toEqual([]);

  // Step 4: fetch stories
  result.current.handleFetch();

  // Step 5: test after load axios api
  await waitForNextUpdate();
  expect(result.current.stories).toEqual(mockStory);
});

React Component

測(cè)試完 Hook恼布,我們把它放到組件里。老規(guī)矩搁宾,先不看實(shí)現(xiàn)折汞,看效果:

FetchButton

簡(jiǎn)單來說就是寫了一個(gè) button,點(diǎn)擊后會(huì)異步調(diào)用數(shù)據(jù)盖腿,然后展示出所有的 stories 條目爽待。

還是照搬上面的套路:

  1. 利用 jest.mock 覆蓋 axios 模塊
  2. 偽造一個(gè) axios.get 的 response
  3. 點(diǎn)擊按鈕,即異步加載 stories
  4. 結(jié)束后翩腐,斷言屏幕上會(huì)出現(xiàn)了相應(yīng)的 story 條目
import axios from "axios";

// Step 1: override a module
jest.mock("axios");

const mockStory = [
  { objectID: "1", title: "Hello" },
  { objectID: "2", title: "React" },
];

describe("FetchButton", () => {
  it("fetches stories from an API and displays them", async () => {
    render(<FetchButton />);

    // Step 2: fake a response
    const response = { data: mockStory };
    axios.get.mockResolvedValue(response);

    // Step 3: click button
    userEvent.click(screen.getByRole("button"));

    // Step 4: assert that screen will display 2 items
    const $items = await screen.findAllByRole("listitem");
    expect($items).toHaveLength(2);
  });
});

教課書里的 TDD 要求先寫測(cè)試鸟款,再寫實(shí)現(xiàn)的。我這里也盡量按照這個(gè)思路排版茂卦,大家看完單元測(cè)試何什,也應(yīng)該有了大體的組件實(shí)現(xiàn)輪廓了吧?我把我的實(shí)現(xiàn)寫出來:

// FetchButton.js
import { useStory } from "./useStory";

export function FetchButton() {
  const { error, stories, handleFetch } = useStory();

  return (
    <div>
      <button onClick={handleFetch}>Fetch Stories</button>

      {error && <span>Something went wrong ...</span>}

      <ul>
        {stories.map((story) => (
          <li key={story.objectID}>{story.title}</li>
        ))}
      </ul>
    </div>
  );
}

異常測(cè)試

我們接著說上面的 FetchButton疙筹。在寫實(shí)現(xiàn)的時(shí)候富俄,我發(fā)現(xiàn)了之前測(cè)試?yán)镉袀€(gè)重大疏漏:異步調(diào)取的 API 可能會(huì)返回錯(cuò)誤,之前的單元測(cè)試沒有覆蓋到拋異常的情況而咆。所以我又在實(shí)現(xiàn)里加了個(gè) error 的判斷霍比;有錯(cuò)誤就顯示 Something went wrong ...嗜愈。既然有疏漏偿曙,就繼續(xù)補(bǔ) test case。這種測(cè)試其實(shí)更簡(jiǎn)單宵睦,基本上就是套用上文的步驟涯捻,唯一不同之處就是讓 axios.get 返回mockRejectedValue浅妆。大家看看下面的 test case,應(yīng)該已經(jīng)沒有難點(diǎn)了障癌。

jest.mock("axios");

describe("FetchButton", () => {

  it("fetch stories from an API but fail", async () => {

    render(<FetchButton />);

+    axios.get.mockRejectedValue(new Error()));

    userEvent.click(screen.getByRole("button"));

    const $errorMsg = await screen.findByText(/Something went wrong/);

    expect($errorMsg).toBeInTheDocument();
  });
});

小結(jié)

我最近又回看了2020 年 JS 滿意度調(diào)查凌外,發(fā)現(xiàn) Testing Library 位居測(cè)試榜榜首,還是很有群眾基礎(chǔ)的涛浙。我們用了兩期時(shí)間介紹了 Testing Library 的入門教程康辑,也希望大家能盡快把 TDD 落實(shí)到自己的項(xiàng)目中。我見過好多項(xiàng)目幾乎沒有任何測(cè)試轿亮,一兩年就爛掉了疮薇;一加新功能就四處漏風(fēng),后期 bug 數(shù)量急速上升我注,release 甚至能因此拖延半年之久按咒;屎山一堆積,想改就再也改不了了但骨。TDD 是軟件行業(yè)多年來的最佳實(shí)踐励七,我們作為該行業(yè)的從業(yè)人員智袭,也應(yīng)該堅(jiān)定地按行業(yè)規(guī)律辦事;這不僅是“干活勤快”這么簡(jiǎn)單呀伙,更多的是自己職業(yè)素養(yǎng)的體現(xiàn)补履,共勉。

State of JS 2020

相關(guān)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末剿另,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贬蛙,更是在濱河造成了極大的恐慌雨女,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阳准,死亡現(xiàn)場(chǎng)離奇詭異氛堕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)野蝇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門讼稚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绕沈,你說我怎么就攤上這事锐想。” “怎么了乍狐?”我有些...
    開封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵赠摇,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我浅蚪,道長(zhǎng)藕帜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任惜傲,我火速辦了婚禮洽故,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盗誊。我一直安慰自己时甚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開白布浊伙。 她就那樣靜靜地躺著撞秋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嚣鄙。 梳的紋絲不亂的頭發(fā)上吻贿,一...
    開封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音哑子,去河邊找鬼舅列。 笑死肌割,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的帐要。 我是一名探鬼主播把敞,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼榨惠!你這毒婦竟也來了奋早?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤赠橙,失蹤者是張志新(化名)和其女友劉穎耽装,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體期揪,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掉奄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凤薛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姓建。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖缤苫,靈堂內(nèi)的尸體忽然破棺而出速兔,到底是詐尸還是另有隱情,我是刑警寧澤榨馁,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布憨栽,位于F島的核電站,受9級(jí)特大地震影響翼虫,放射性物質(zhì)發(fā)生泄漏屑柔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一珍剑、第九天 我趴在偏房一處隱蔽的房頂上張望掸宛。 院中可真熱鬧,春花似錦招拙、人聲如沸唧瘾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饰序。三九已至,卻和暖如春规哪,著一層夾襖步出監(jiān)牢的瞬間求豫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝠嘉,地道東北人最疆。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蚤告,于是被迫代替她去往敵國(guó)和親努酸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容