用React hooks實現TDD

本文首發於個人我的博客: https://teobler.com, 轉載請註明出處

因爲篇幅所限文章中並無給出demo的全部代碼,你們若是有興趣能夠將代碼clone到本地從commit來看整個demo的TDD過程,配合文章來看會比較清晰。本文涉及的全部代碼地址: teobler/TDD-with-React-hooks-demo前端

前端TDD的痛

從進公司前認識了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();
};

純UI組件

這類組件咱們沒有一個個去測試組件裏面的元素,而是按照UX的要求build完組件之後加上一個jest的json snapshot測試。函數

注意這裏的snapshot並非你們印象中的e2e測試中的截圖,而是jest裏將組件render出來以後使用json生成一份UI的dom結構,在下次測試時,生成一份新的快照與舊的快照進行比對,從而得出兩個UI不同的地方,實現對UI的保護。

可是其實使用snapshot測試有兩個問題:單元測試

  1. snapshot相比較於通常的單元測試來講運行速度較慢,若是項目中大量使用的snapshot測試的話,在運行全部單元測試的時候會比較明顯的感覺到單元測試的速度被拖慢了,必定程度上違背了單元測試快速反饋的初衷;
  2. 維護snapshot的人工成本較大,snapshot測試最大的問題在於你只要改動了任何UI的部分,這個測試都會掛掉,這個時候就須要仔細對比不一樣的地方以決定是更新snapshot仍是改錯地方了,而若是此時團隊裏有「省心」的隊友無腦更新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>
        );
    };

邏輯與UI混合組件

這個部分咱們就須要hooks的幫忙了,這樣的組件不是UI和邏輯強耦合嘛,那咱們就能夠將二者拆開。因而這樣的組件咱們會這樣寫:測試

  1. 首先將UI頁面build出來,可是須要的callback所有寫成空函數
  2. 將全部callback或者是頁面須要用到的邏輯抽到一個hook中
  3. 此時hook裏的代碼沒有UI只有邏輯,故可使用測試庫對hook進行單獨的邏輯測試,因此此時hook的開發能夠按照邏輯組件的開發進行TDD
  4. 整個混合組件開發完成後,補上一個snapshot測試,須要注意的是可能該組件在渲染時須要一些數據,在寫snapshot測試時應該確保準備的數據是完備的,不然快照會渲染出一份根本沒有數據的錯誤組件
// 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的測試),你們若是有更好的實踐的話歡迎一塊兒討論。

相關文章
相關標籤/搜索