摘要:單元測試針對程序模塊進行測試。是開源的單元測試工具。一個好的單元測試應該具備的條件安全重構已有代碼單元測試一個很重要的價值是為重構保駕護航。斷言外部依賴單元測試的一個重要原則就是無依賴和隔離。
前端測試金字塔
對于一個 Web 應用來說,理想的測試組合應該包含大量單元測試(unit tests),部分快照測試(snapshot tests),以及少量端到端測試(e2e tests)。參考測試金字塔,我們構建了前端應用的測試金字塔。
單元測試
針對程序模塊進行測試。模塊是軟件設計中的最小單位,一個函數或者一個 React 組件都可以稱之為一個模塊。單元測試運行快,反饋周期短,在短時間內就能夠知道是否破壞了代碼,因此在測試組合中占據了絕大部分。
快照測試
對組件的 UI 進行測試。傳統的快照測試會拍攝組件的圖片,并且將它和之前的圖片進行對比,如果兩張圖片不匹配則測試失敗。Jest 的快照測試不會拍攝圖片,而是將 React 樹序列化成字符串,通過比較兩個字符串來判斷 UI 是否改變。因為是純文本的對比,所以不需要構建整個應用,運行速度自然比傳統快照測試更快。
E2E 測試
相當于黑盒測試。測試者不需要知道程序內部是如何實現的,只需要根據業務需求,模擬用戶的真實使用場景進行測試。
測試種類 | 技術選型 |
---|---|
單元測試 | Jest + Enzyme |
快照測試 | Jest |
E2E 測試 | jest-puppeteer |
Jest?是 Facebook 開源的測試框架。它的功能很強大,包含了測試執行器、斷言庫、spy、mock、snapshot 和測試覆蓋率報告等。
Enzyme?是 Airbnb 開源的 React 單元測試工具。它擴展了 React 官方的 TestUtils,通過類 jQuery 風格的 API?對 DOM 進行處理,減少了很多重復代碼,可以很方便的對渲染出來的結果進行斷言。
jest-p[uppeteer]()?是一個同時包含 Jest 和 Puppeteer 的工具。Puppeteer 是谷歌官方提供的 Headless?Chrome Node API,它提供了基于?DevTools Protocol?的上層 API 接口,用來控制?Chrome 或者 Chromium。有了?Puppeteer,我們可以很方便的進行端到端測試。
測試本質上是對代碼的保護,保證項目在迭代的過程中正常運行。當然,寫測試也是有成本的,特別是復雜邏輯,寫測試花的時間,可能不比寫代碼少。所以我們要制定合理的測試策略,有針對性的去寫測試。至于哪些代碼要測,哪些代碼不測,總的來說遵循一個原則:投入低,收益高。「投入低」是指測試容易寫,「收益高」是測試的價值高。換句話說,就是指測試應該優先保證核心代碼邏輯,比如核心業務、基礎模塊、基礎組件等,同時,編寫測試和維護測試的成本也不宜過高。當然,這是理想情況,在實際的開發過程中還是要進行權衡。
單元測試基于 React 和 Redux 項目的特點,我們制定了下面的測試策略:
分類 | 哪些要測? | 哪些不測? |
---|---|---|
組件 |
有條件渲染的組件(如 if-else 分支,聯動組件,權限控制組件等) 有用戶交互的組件(如 Click、提交表單等) * 邏輯組件(如高階組件和 Children Render 組件) |
connect 生成的容器組件 純組合子組件的 Page 組件 純展示的組件 組件樣式 |
Reducer | 有邏輯的 Reducer。如合并、刪除? state。 | 純取值的 reducer 不測。比如 (_, action) => action.payload.data? |
Middleware | 全測 | 無 |
Action Creator | 無 | 全不測 |
方法 |
validators formatters * 其他公有方法 |
私有方法 |
公用模塊 | 全測。比如處理 API 請求的模塊。 | 無 |
Note: 如果使用了 TypeScript,類型約束可以替代部分函數入參和返回值類型的檢查。快照測試
Jest 的 snapshot 測試雖然運行起來很快,也能夠起到一定保護 UI 的作用。但是它維護起來很困難(大量依賴人工對比),并且有時候不穩定(UI 無變化但 className 變化仍然會導致測試失敗)。因此,個人不推薦在項目中使用。但是為了應付測試覆蓋率,以及「給自己信心」,也可以給以下部分添加?snapshot 測試:
Page 組件:一個 page 對應一個 snapshot。
純展示的公用 UI 組件。
快照測試可以等整個 Page 或者 UI 組件構建完成之后再添加,以保證穩定。
E2E 測試覆蓋核心的業務 flow。
一個好的單元測試應該具備的條件? 安全重構已有代碼單元測試一個很重要的價值是為重構保駕護航。當輸入不變時,當且僅當「被測業務代碼功能被改動了」時,測試才應該掛掉。也就是說,無論怎么重構,測試都不應該掛掉。
在寫組件測試時,我們常常遇到這樣的情況:用 css class 選擇器選中一個節點,然后對它進行斷言,那么即使業務邏輯沒有發生變化,重命名這個 class 時也會使測試掛掉。理論上來說,這樣的測試并不算一個「好的測試」,但是考慮到它的業務價值,我們還是會寫一些這樣的測試,只不過寫測試的時候需要注意:使用一些不容易發生變化的選擇器,比如 component name、arial-label 等。
保存業務上下文我們經常說測試即文檔,沒錯,一個好的測試往往能夠非常清晰的表單業務或代碼的含義。
快速回歸快速回歸是指測試運行速度快,且穩定。要想運行速度快,很重要的一點是 mock 好外部依賴。至于怎么具體怎么 mock 外部依賴,后面會詳細說明。
單元測試怎么寫? 定義測試名稱建議采用?BDD?的方式,即測試要接近自然語言,方便團隊中的各個成員進行閱讀。編寫測試用例的時候,可以參考 AC,試著將 AC 的 Give-When-Then 轉化成測試用例。
GIVEN: 準備測試條件,比如渲染組件。
WHEN:在某個具體的場景下,比如點擊 button。
THEN:斷言
describe("add user", () => { it("when I tap add user button, expected dialog opened with 3 form fields", () => { // Given: in profile page. // Prepare test env, like render component etc. // When: button click. // Simulate button click // Then: display `add user` form, which contains username, age and phone number. // Assert form fields length to equal 3 }); });Mock 外部依賴
單元測試的一個重要原則就是無依賴和隔離。也就是說,在測試某部分代碼時,我們不期望它受到其他代碼的影響。如果受到外部因素影響,測試就會變得非常復雜且不穩定。
我們寫單元測試時,遇到的最大問題就是:代碼過于復雜。比如當頁面有 API 請求、日期、定時器或 redux conent 時,寫測試就變得異常困難,因為我們需要花大量時間去隔離這些外部依賴。
隔離外部依賴需要用到測試替代方法,常見的有 spies、stubs 和 mocks。很多測試框架都實現了這三種方法,比如著名的 Jest 和 Sinon。這些方法可以幫助我們在測試中替換代碼,減少測試編寫的復雜度。
spiesspies 本質上是一個函數,它可以記錄目標函數的調用信息,如調用次數、傳參、返回值等等,但不會改變原始函數的行為。Jest 中的?mock function?就是 spies,比如我們常用的?jest.fn()?。
// Example: onSubmit() { // some other logic here this.props.dispatch("xxx_action"); } // Example Test: it("when form submit, expected dispatch function to be called", () => { const mockDispatch = jest.fn(); mount(); // simlate submit event here expect(mockDispatch).toBeCalledWith("xxx_action"); expect(mockDispatch).toBeCalledTimes(1); });
spies 還可以用于替換屬性方法、靜態方法和原型鏈方法。由于這種修改會改變原始對象,使用之后必須調用 restore 方法予以還原,因此使用的時候要特別小心。
// Example: const video = { play() { return true; }, }; // Example Test: test("plays video", () => { const spy = jest.spyOn(video, "play"); const isPlaying = video.play(); expect(spy).toHaveBeenCalled(); expect(isPlaying).toBe(true); spy.mockRestore(); });stubs
stubs 跟 spies 類似,但與 spies 不同的是,stubs 會替換目標函數。也就是說,如果使用 spies,原始的函數依然會被調用,但使用 stubs,原始的函數就不會被執行了。stubs 能夠保證明確的測試邊界。它可以用于以下場景:
替換讓測試變得復雜或慢的外部函數,如 ajax。
測試異常條件,如拋出異常。
Jest 中也提供了類似的 API [](https://jestjs.io/docs/en/jes...[]()jest.spyOn().mockImplementation(),如下:
const spy = jest.fn(); const payload = [1, 2, 3]; jest .spyOn(jQuery, "ajax") .mockImplementation(({ success }) => success(payload)); jQuery.ajax({ url: "https://example.api", success: data => spy(data) }); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(payload);mocks
mocks 是指用自定義對象代替目標對象。我們不僅可以 mock API 返回值和自定義類,還可以 mock npm 模塊等等。
// mock middleware api const mockMiddlewareAPI = { dispatch: jest.fn(), getState: jest.fn(), }; // mock npm module `config` jest.mock("config", () => { return { API_BASE_URL: "http://base_url", }; });
使用 mocks 時,需要注意:
如果 mock 了某個模塊的依賴,需要等 mock 完成了之后再 require 這個模塊。
有如下代碼:
// counter.ts let count = 0; export const get = () => count; export const inc = () => count++; export const dec = () => count--;
錯誤做法:
// counter.test.ts import * as counter from "../counter"; describe("counter", () => { it("get", () => { jest.mock("../counter", () => ({ get: () => "mock count", })); expect(counter.get()).toEqual("mock count"); // 測試失敗,此時的 counter 模塊并非 mock 之后的模塊。 }); });
正確做法:
describe("counter", () => { it("get", () => { jest.mock("../counter", () => ({ get: () => "mock count", })); const counter = require("../counter"); // 這里的 counter 是 mock 之后的 counter expect(counter.get()).toEqual("mock count"); // 測試成功 }); });
多個測試有共享狀態時,每次測試完成之后需要重置模塊 jest.resetModules()?。它會清空所有 required 模塊的緩存,保證模塊之間的隔離。
錯誤的做法:
describe("counter", () => { it("inc", () => { const counter = require("../counter"); counter.inc(); expect(counter.get()).toEqual(1); }); it("get", () => { const counter = require("../counter"); // 這里的 counter 和上一個測試中的 counter 是同一份拷貝 expect(counter.get()).toEqual(0); // 測試失敗 console.log(counter.get()); // ? 輸出: 1 }); });
正確的做法:
describe("counter", () => { afterEach(() => { jest.resetModules(); // 清空 required modules 的緩存 }); it("inc", () => { const counter = require("../counter"); counter.inc(); expect(counter.get()).toEqual(1); }); it("get", () => { const counter = require("../counter"); // 這里的 counter 和上一個測試中的 counter 是不同的拷貝 expect(counter.get()).toEqual(0); // 測試成功 console.log(counter.get()); // ? 輸出: 0 }); });
修改代碼,從一個外部模塊 defaultCount 中獲取?count 的默認值。
// defaultCount.ts export const defaultCount = 0; // counter.ts import {defaultCount} from "./defaultCount"; let count = defaultCount; export const inc = () => count++; export const dec = () => count--; export const get = () => count;
測試代碼:
import * as counter from "../counter"; // 首次導入 counter 模塊 console.log(counter); describe("counter", () => { it("inc", () => { jest.mock("../defaultCount", () => ({ defaultCount: 10, })); const counter1 = require("../counter"); // 再次導入 counter 模塊 counter1.inc(); expect(counter1.get()).toEqual(11); // 測試失敗 console.log(counter1.get()); // 輸出: 1 }); });
再次 require counter 時,發現模塊已經被 require 過了,就直接從緩存中獲取,所以 counter1 使用的還是counter 的上下文,也就是 defaultCount = 0。而調用?resetModules() 會清空 cache,重新調用模塊函數。
在上面的代碼中,注釋掉 1,2 行,測試也會成功。大家可以想想為什么?編寫測試 組件測試
要對組件進行測試,首先要將組件渲染出來。Enzyme 提供了三種渲染方式: 淺渲染、全渲染以及靜態渲染。
shallow 方法會把組件渲染成 Virtual DOM 對象,只會渲染組件中的第一層,不會渲染它的子組件,因此不需要關心 DOM 和執行環境,測試的運行速度很快。
淺渲染對上層組件非常有用。上層組件往往包含很多子組件(比如 App 或 Page 組件),如果將它的子組件全部渲染出來,就意味著上層組件的測試要依賴于子組件的行為,這樣不僅使測試變得更加困難,也大大降低了效率,不符合單元測試的原則。
淺渲染也有天生的缺點,因為它只能渲染一級節點。如果要測試子節點,又不想全渲染怎么辦呢?shallow?還提供了一個很好用的接口 .dive,通過它可以獲取 wrapper 子節點的 React DOM 結構。
示例代碼:
export const Demo = () => ();
使用 shallow?后得到如下結構:
使用 .dive()?后得到如下結構:
mount 方法會把組件渲染成真實的 DOM 節點。如果你的測試依賴于真實的 DOM 節點或者子組件,那就必須使用 mount 方法。特別是大量使用 Child Render 的組件,很多時候測試會依賴 Child Render 里面的內容,因此需要需要用全渲染,將子組件也渲染出來。
全渲染方式需要瀏覽器環境,不過 Jest 已經提供了,它的默認的運行環境 jsdom?,就是一個 JavaScript 瀏覽器環境。需要注意的是,如果多個測試依賴了同一個 DOM,它們可能會相互影響,因此在每個測試結束之后,最好使用 .unmount()?進行清理。
將組件渲染成靜態的 HTML 字符串,然后使用 Cheerio?對其進行解析,返回一個 Cheerio 實例對象,可以用來分析組件的 HTML 結構。
我們常常會用到條件渲染,也就是在滿足不同條件時,渲染不同組件。比如:
?
import React, { ReactNode } from "react"; const Container = ({ children }: { children: ReactNode }) =>{children}; const CompA = ({ children }: { children: ReactNode }) =>{children}; const List = () =>List Component; interface IDemoListProps { list: string[]; } export const DemoList = ({ list }: IDemoListProps) => (); {list.length > 0 ? : null}
對于條件渲染,這里提供了兩種思路:
測試是否渲染了正確節點
一般的做法是將?DemoList 組件渲染出來,再根據不同的條件,去檢查是否渲染出了正確的節點。
describe("DemoList", () => { it("when list length is more than 0, expected to render List component", () => { const wrapper = shallow(); expect( wrapper .dive() .find("List") .exists(), ).toBe(true); }); it("when list length is more than 0, expected to render null", () => { const wrapper = shallow( ); expect( wrapper .dive() .find("[aria-label="container"]") .children().length, ).toBe(0); }); });
公用組件 + 只測判斷條件
我們可以抽象一個公用組件
我們可以為這個組件添加測試,確保在不同的條件下顯示正確的節點。既然這個邏輯得已經得到了保證,使用
export const shouldShowBtn = (a: string, b: string, c: string) => a === b || b === c;
describe("should show button or not", () => { it("should show button", () => { expect(shouldShowBtn("x", "x", "x")).toBe(true); }); it("should hide button", () => { expect(shouldShowBtn("x", "y", "z")).toBe(false); }); });
對于有權限控制的組件,一個小的配置改變也會導致整個渲染的不同,而且人工測試很難發現,這種配置多一個 prop 檢查會讓代碼更加安全。
常見的有點擊事件、表單提交、validate 等。
點擊事件 click。
onSubmit?。主要是測試 onSubmit?方法被調用之后是否發生了正確的行為,如 dispatch action 。
validate?。 主要是測試 error message 是否按正確的順序顯示。
Action Creator 測試action creator 的實現和測試都非常簡單,這里就不舉例了。但要注意的是,不要將計算邏輯放到 aciton creator 中。
錯誤的方式:
// action.ts export const getList = createAction("@@list/getList", (reqParams: any) => { const params = formatReqParams({ ...reqParams, page: reqParams.page + 1, startDate: formatStartDate(reqParams.startDate) endDate: formatStartDate(reqParams.endDate) }); return { url: "/api/list", method: "GET", params, }; });
正確的方式:
// action.ts export const getList = createAction("@@list/getList", (params: any) => { return { url: "/api/list", method: "GET", params, }; }); // 調用 action creator 時,先把值計算好,再傳給 action creator。 // utils.ts const formatReqParams = (reqParams: any) => { return formatReqParams({ ...reqParams, page: reqParams.page + 1, startDate: formatStartDate(reqParams.startDate) endDate: formatStartDate(reqParams.endDate) }); }; // page.ts getFeedbackList(formatReqParams({}));Reducer 測試
Reducer 測試主要是測試「根據 Action 和 State 是否生成了正確的 State」。因為 reducer 是純函數,所以測試非常好寫,這里就不細講了。?
Middleware 測試測試 middleware 最重要的就是 mock 外部依賴,其中包括 middlewareAPI?和 next?。
Test Helper:
class MiddlewareTestHelper { static of(middleware: any) { return new MiddlewareTestHelper(middleware); } constructor(private middleware: Middleware) {} create() { const middlewareAPI = { dispatch: jest.fn(), getState: jest.fn(), }; const next = jest.fn(); const invoke$ = (action: any) => this.middleware(middlewareAPI)(next)(action); return { middlewareAPI, next, invoke$, }; } }
Example Test:
it("should handle the action", () => { const { next, invoke$ } = MiddlewareTestHelper.of(testMiddleware()).create(); invoke$({ type: "SOME_ACTION", payload: {}, }); expect(next).toBeCalled(); });測試異步代碼
默認情況下,一旦到達運行上下文底部,jest測試立即結束。為了解決這個問題,我們可以使用:
done() 回調函數
return promise
async/await
錯誤的方式:
test("the data is peanut butter", () => { function callback(data) { expect(data).toBe("peanut butter"); } fetchData(callback); });
正確的方式:
test("the data is peanut butter", done => { function callback(data) { expect(data).toBe("peanut butter"); done(); } fetchData(callback); });
test("the data is peanut butter", () => { expect.assertions(1); return fetchData().then(data => { expect(data).toBe("peanut butter"); }); });
test("the data is peanut butter", async () => { const data = await fetchData(); expect(data).toBe("peanut butter"); });執行測試
采用「紅 - 綠」的方式,即先讓測試失敗,再修改代碼讓測試通過,以確保斷言被執行。
快照測試怎么寫?通過 redux-mock-store,將組件需要的全部數據準備好(給 mock store 準備 state),再進行測試。
從測試的角度反思應用設計「好測試」的前提是要有「好代碼」。因此我們可以從測試的角度去反思整個應用的設計,讓組件的「可測試性」更高。
單一職責。 一個組件只干一類事情,降低復雜度。只要每個小的部分能夠被正確驗證,組合起來能夠完成整體功能,那么測試的時候,只需要專注于各個小的部分即可。
良好的復用。 即復用邏輯的同時,也復用了測試。
保證最小可用,再逐漸增加功能。 也就是我們平時所說的 TDD。
...
Debugconsole.log(wrapper.debug());參考文章?
譯-Sinon入門:利用Mocks,Spies和Stubs完成javascript測試
使用Jest進行React單元測試
對 React 組件進行單元測試
How to Rethink Your Testing
使用Enzyme測試React(Native)組件
Node.js模塊化機制原理探究
單元測試的意義、做法、經驗
React 單元測試策略及落地?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/8902.html
摘要:的組件開發一直處在一個比較尷尬的處境。目錄包含了當前組件的源碼,是組件開發最主要的目錄。許多的開發者對于依然持懷疑態度。 React Native的組件開發一直處在一個比較尷尬的處境。在官方未給予相關示例與腳手架的情況下,社區中依然誕生了許許多多的React Native組件。因為缺少示例與規范,很多組件庫僅含有一個index.js文件。這種基礎的目錄結構也導致了一些顯而易見的問題,例...
摘要:特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 本以為自己收藏的站點多,可以很快搞定,沒想到一入匯總深似海。還有很多不足&遺漏的地方,歡迎補充。有錯誤的地方,還請斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應和斧正,會及時更新,平時業務工作時也會不定期更...
摘要:以下內容來自我特別喜歡的一個頻道這是一個年你成為前端,后端或全棧開發者的進階指南你不需要學習所有的技術成為一個開發者這個指南只是通過簡單分類列出了技術選項我將從我的經驗和參考中給出建議首選我們會介紹通用的知識最后介紹年的的一些趨勢基礎前端開 以下內容來自我特別喜歡的一個Youtube頻道: Traversy Media 這是一個2019年你成為前端,后端或全棧開發者的進階指南: 你...
摘要:前端每周清單年度總結與盤點在過去的八個月中,我幾乎只做了兩件事,工作與整理前端每周清單。本文末尾我會附上清單線索來源與目前共期清單的地址,感謝每一位閱讀鼓勵過的朋友,希望你們能夠繼續支持未來的每周清單。 showImg(https://segmentfault.com/img/remote/1460000010890043); 前端每周清單年度總結與盤點 在過去的八個月中,我幾乎只做了...
摘要:感謝王下邀月熊分享的前端每周清單,為方便大家閱讀,特整理一份索引。王下邀月熊大大也于年月日整理了自己的前端每周清單系列,并以年月為單位進行分類,具體內容看這里前端每周清單年度總結與盤點。 感謝 王下邀月熊_Chevalier 分享的前端每周清單,為方便大家閱讀,特整理一份索引。 王下邀月熊大大也于 2018 年 3 月 31 日整理了自己的前端每周清單系列,并以年/月為單位進行分類,具...
閱讀 3514·2023-04-25 20:09
閱讀 3720·2022-06-28 19:00
閱讀 3035·2022-06-28 19:00
閱讀 3058·2022-06-28 19:00
閱讀 3132·2022-06-28 19:00
閱讀 2859·2022-06-28 19:00
閱讀 3014·2022-06-28 19:00
閱讀 2610·2022-06-28 19:00