本文首發於個人我的博客: https://teobler.com, 轉載請註明出處
因爲篇幅所限文章中並無給出demo的全部代碼,你們若是有興趣能夠將代碼clone到本地從commit來看整個demo的TDD過程,配合文章來看會比較清晰。本文涉及的全部代碼地址: teobler/TDD-with-React-hooks-demo前端
從進公司前認識了TDD,到實踐TDD,過程當中本身遇到或者小夥伴們一塊兒討論的比較頻繁的一個問題是 — 前端不太好TDD / 前端TDD的投入收益比不高。爲啥會這樣呢?react
咱們假設你在寫前端時全程TDD,那麼你須要作的是 — 先assert頁面上有一個button,而後去實現這個button,以後assert點擊這個button以後會發生什麼,最後再去實現相應的邏輯。git
這個過程當中有一個問題,由於前端中UI和邏輯強耦合,因此在TDD的時候你須要先實現UI,而後選中這個UI上的組件,trigger相應的行爲,這個過程給開發人員增長了很多負擔。github
誠然,這樣寫出來的代碼嚴格遵循了TDD的作法,也獲得了TDD給咱們帶來的各類好處,可是據我觀察下來,身邊的小夥伴們沒有一我的認同這樣的作法。你們的痛點在於UI部分的TDD過於痛苦而且收益過低,並且因爲UI和邏輯強耦合,後續的邏輯部分也須要先選取頁面上的元素trigger出相應的執行邏輯。typescript
這些痛點在項目組引入了hooks以後有了顯著的改善,從引入hooks到如今快一年的時間,組裏的小夥伴們一塊兒總結除了一套測試策略。在此咱們將React的組件分爲三類 — 純邏輯組件(好比request的處理組件,utils函數等),純UI組件(好比展現用的Layout,Container組件等)和二者結合的混合組件(好比某個頁面)。json
這部分組件沒啥好說的,全都是邏輯,tasking,測試,實現,重構一條龍,具體咋寫咱們這裏不討論。dom
// combineClass.test.ts describe('combineClass', () => { it('should return prefixed string given only one class name', () => { const result = combineClass('class-one'); expect(result).toEqual('prefix-class-one'); }); it('should trim space for class name', () => { const result = combineClass('class-one '); expect(result).toEqual('prefix-class-one'); }); it('should combine two class name and second class name should not add prefix', () => { const result = combineClass('class-one', 'class-two'); expect(result).toEqual('prefix-class-one class-two'); }); it('should combine three class name and tail class name should not add prefix', () => { const result = combineClass('class-one', 'class-two', 'class-three'); expect(result).toEqual('prefix-class-one class-two class-three'); }); }); // combineClass.ts const CLASS_PREFIX = "prefix-"; export const combineClass = (...className: string[]) => { const resultName = className.slice(0); resultName[0] = CLASS_PREFIX + className[0]; return resultName .join(' ') .trim(); };
這類組件咱們沒有一個個去測試組件裏面的元素,而是按照UX的要求build完組件之後加上一個jest的json snapshot測試。函數
注意這裏的snapshot並非你們印象中的e2e測試中的截圖,而是jest裏將組件render出來以後使用json生成一份UI的dom結構,在下次測試時,生成一份新的快照與舊的快照進行比對,從而得出兩個UI不同的地方,實現對UI的保護。
可是其實使用snapshot測試有兩個問題:單元測試
// Content.test.tsx describe('Content', () => { it('should render correctly', () => { const {container} = render(<Content/>); expect(container).toMatchSnapshot(); }); }); // Content.test.tsx.snap // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Content should render correctly 1`] = ` <div> <main class="prefix-layout-content" /> </div> `; // Content.tsx export const Content: React.FC<React.HTMLAttributes<HTMLElement>> = (props) => { const { className = '', children, ...restProps } = props; return ( <main className={combineClass('layout-content', className)} {...restProps}> {children} </main> ); };
這個部分咱們就須要hooks的幫忙了,這樣的組件不是UI和邏輯強耦合嘛,那咱們就能夠將二者拆開。因而這樣的組件咱們會這樣寫:測試
// usePageExample.test.ts import {act, renderHook} from "@testing-library/react-hooks"; describe('usePageExample', () => { let mockGetUserId: jest.Mock; let mockValidate: jest.Mock; beforeAll(() => { mockGetUserId = jest.fn(); mockValidate = jest.fn(); jest.mock('../../../../request/someRequest', () => ({ getUserId: mockGetUserId, })); jest.mock('../../../../validator/formValidator', () => ({ formValidate: mockValidate, })); }); afterAll(() => { mockGetUserId.mockReset(); mockValidate.mockReset(); }); it('should trigger request with test string when click button', () => { const {usePageExample} = require('../usePageExample'); const {result} = renderHook(() => usePageExample()); act(() => { result.current.onClick(); }); expect(mockGetUserId).toBeCalled(); }); it('should validate form values before submit', () => { const {usePageExample} = require('../usePageExample'); const {result} = renderHook(() => usePageExample()); const formValues = {id: '1', name: 'name'}; act(() => { result.current.onSubmit(formValues); }); expect(mockValidate).toBeCalledWith(formValues); }); }); // usePageExample.ts import {getUserId} from "../../../request/someRequest"; import {formValidate} from "../../../validator/formValidator"; export interface IFormValues { email: string; name: string; } export const usePageExample = () => { const onClick = () => { getUserId(); }; const onSubmit = (formValues: IFormValues) => { formValidate(formValues); }; return {onClick, onSubmit}; }; // PageExample.tsx import * as React from "react"; import {usePageExample} from "./hooks/usePageExample"; export const PageExample: React.FC<IPageExampleProps> = () => { const {onClick, onSubmit} = usePageExample(); return ( <div> <form onSubmit={() => onSubmit}> <input type="text"/> </form> <button onClick={onClick}>test</button> </div> ); };
這篇文章算是給你們提供了一個hooks的TDD思路,固然其中還有一些咱們也以爲不是很完善的地方(好比UI的測試),你們若是有更好的實踐的話歡迎一塊兒討論。