React組件測試

前言:有了前面Jest和Enzyme基礎知識的儲備,接下來,我們看如何在項目中測試組件。

一.測什麼和如何測?

當我們看到一個組件時,首要考慮測試什麼?然後纔是如何測試。作爲一個通用的測試原則,我是基於這幾個方面進行考慮。

下面所有測試代碼,都是針對TodoList這個示例demo,界面如下:
在這裏插入圖片描述
功能說明:在Add Todo這個input框中輸入內容,點擊回車時,內容添加到正在進行這個列表中,同時還要進行個數統計,並且清空input框。

  • 組件節點測試
    其實就是隻關注render函數中,組件的渲染情況就可以了。
    在這裏插入圖片描述
    上面的render函數是有輸入框的Header組件,接下來我們根據渲染,編寫測試用例。
    在這裏插入圖片描述
    雖然測試用例測試的方面,組件渲染出來時,我們一眼就能看出來,是否滿足需求,但還是建議編寫測試代碼。因爲,當我們項目非常龐大時,很可能在不經意間修改了代碼,比如修改了placeholder,我們可能不容易發現這種修改,這時候,測試用例就會不通過,除非我們真的想要改變placeholder,此時,可以修改測試用例,讓測試通過。這樣,就可以很好的避免誤修改。

  • 交互測試
    主要是利用simulate()函數模擬事件,實際上simulate是通過觸發事件綁定函數,來模擬事件的觸發。觸發事件後,去判斷props上特定函數是否被調用,傳參是否正確;組件狀態是否發生預料之中的修改;某個dom節點是否存在,是否符合期望。

import React, { Component } from 'react';

class Header extends Component {
    constructor(props) {
        super(props);
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
        this.state = {
            value: ''
        }
    }

    handleInputChange(e) {
        this.setState({
            value: e.target.value
        })
    }

    handleInputKeyUp(e) {
        const { value } = this.state;
        if (e.keyCode === 13 && value) {
            this.props.addUndoItem(value);
            this.setState({ value: '' });
        }
    }

    render() {
        const { value } = this.state;
        return (
            <div className="header">
                <div className="header-content">
                    TodoList
                <input
                    placeholder="Add Todo"
                    className='header-input'
                    data-test='input'
                    value={value}
                    onChange={this.handleInputChange}
                    onKeyUp={this.handleInputKeyUp}
                />
                </div>
            </div>
        )
    }
}

export default Header;

Header組件上有兩個事件處理函數,現在通過交互測試,測試特定事件觸發時,這兩個函數的行爲是否符合預期。
在這裏插入圖片描述
注意:上面的外部傳入函數,addUndoItem,是通過jest.fn()這種形式去模擬的,這是因爲,我們做的只是Header組件的單元測試,我們只要能夠確定,回車時,外部函數被調用了就可以了,至於調用的結果,todoItem是否添加成功,不是Header需要關心的,這是傳入這個函數的組件需要關心的。

  • 測試組件的生命週期函數
    由於生命週期函數是在組件的某一時刻自動調用執行的,是一種自觸發的行爲,所以,我們進行監控。
it('componentDidMount called', () => {
       const componentDidMountSpy = jest.spyOn(TodoList.prototype, 'componentDidMount');
       const wrapper = shallow(<TodoList />);
       expect(componentDidMountSpy).toHaveBeenCalled();
       componentDidMountSpy.mockRestore();
})

注意點:
使用spy替身的時候,在測試用例結束後,要對spy進行restore,不然這個spy會一直存在,並且無法對相同的方法再次進行spy。

使用spyOn來mock 組件的componentDidMount,替身函數要在組件渲染之前,所有替身函數要定義在shallow(<TodoList />)執行之前。

由於生命週期函數是通過componentDidMount()這種形式定義的,所以要對組件的prototype進行mock。

如果僅僅是隻測試組件的生命週期函數,那意義不是很大,就像上面我們只是測試生命週期函數是否執行了。通常我們會有這樣的需求,比如:比如componentDidMount中發送ajax請求,獲取數據。
在這裏插入圖片描述
但在真實的項目測試中,我們並不會去真實的請求後臺接口,用真實的數據測試,因爲這樣會拉低測試的速度。一般,都是和後臺定好契約,然後在測試時,使用mock。

具體怎麼做呢?
上面我們使用axios這個庫進行請求的發送。因此,我們可以mock(模擬)axios。新建一個__mocks__文件夾,然後在裏面新建axios.js文件。模擬代碼如下:

const mockUndoList = {
    data: [{
        status: 'div',
        value: 'afeng hello'
    }],
    success: true
};

export default {
    get(url) {
        if (url === '/undoList.json') {
            return new Promise((resolve, reject) => {
                if (this.success) {
                    resolve(mockUndoList)
                } else {
                    reject(new Error());
                }
            });
        }
    }
}

測試代碼:

import axios from '../../__mocks__/axios';

beforeEach(() => {
  axios.success = true;
})

it(`
  1. 用戶打開頁面,請求正常
  2. 應該展示接口返回的數據
`, (done) => {
  const wrapper = mount(
    <Provider store={store}><TodoList /></Provider>
  );

  process.nextTick(() => {
    wrapper.update();
    const listItem = findTestWrapper(wrapper, 'list-item');
    expect(listItem.length).toBe(1);
    done();
  })
});

測試時,引入mock的axios,這樣組件渲染時,裏面的axios就是我們自已定義的。做到對請求的攔截處理。

注意:上面代碼中,使用了process.nextTick(),這是因爲請求是異步的,我們不能直接在組件渲染後,就去驗證是否達到預期。所以,使用這個或者setTimeout(fn, 0);等請求回來了,setState調用之後,再去驗證。

  • 快照測試
    快照會生成一個組件的UI結構,並用字符串的形式存放在__snapshots__文件裏,通過比較兩個字符串來判斷UI是否改變,因爲是字符串比較,所以性能很高。

要使用快照功能,可以使用enzyme的render靜態渲染一下expect(render(<Header />)).toMatchSnapshot();,jest在執行的時候如果發現toMatchSnapshot方法,會在同級目錄下生成一個__snapshots文件夾用來存放快照文件,以後每次測試的時候都會和第一次生成的快照進行比較。可以使用jest --updateSnapshot來更新快照文件。

應用場景:對於一些公用的組件和UI組件,由於變化的可能性很小,所以,我們可以使用快照固化這個組件。只有當確切的知道修改了,在去更新快照。

二.單元測試和集成測試

首先,瞭解一下。TDD和BDD。

TDD(Testing Driven Development):測試驅動開發。強調的是一種開發方式。以測試來驅動整個項目的開發。即先根據接口,編寫測試用例。然後完成功能開發,目的是要通過所有測試。

一般結合單元測試一起使用。單元測試就是對一個模塊或者組件進行的測試。基本特徵就是隻要輸入不變,必定返回同樣的輸出。一個軟件越容易些單元測試,就表明它的模塊化結構越好,給模塊之間的耦合越弱。React的組件化和函數式編程,天生適合進行單元測試。

優點:
長期減少迴歸bug。
代碼質量更好(組織,可維護性好)
測試覆蓋率高
錯誤測試代碼不容易出現

BDD(Behavior Driven Development):行爲驅動開發。強調的是寫測試的風格,即測試要寫的像自然語言。與TDD的不同,BDD先編寫業務代碼,然後在編寫測試用例。

一般結合集成測試一起使用,集成測試,就是在單元測試的基礎上,將所有子組件組合起來,拼裝成系統或者子系統,然後,測試組合起來,所有的功能是否達到預期。集成測試,一般都是模擬用戶的行爲,是以story的形式進行的。

it(`
  1. Header 輸入框輸入內容
  2. 點擊回車
  3. 列表中展示用戶輸入的內容項
`, () => {
        const wrapper = mount(
            <Provider store={store}><TodoList /></Provider>
        );
        const inputElem = findTestWrapper(wrapper, 'header-input');
        const content = "afeng"
        inputElem.simulate('change', {
            target: { value: content }
        });
        inputElem.simulate('keyUp', {
            keyCode: 13
        });
        const listItem = findTestWrapper(wrapper, 'list-item');
        expect(listItem.length).toEqual(1);
        expect(listItem.text()).toContain(content);
    });

三.總結

說了這麼多,是時候總結一下,最佳實踐了。

一般而言,當我們開發一個個小的組件時,建議進行單元測試,首先保證,外部調用時,這個組件是好用的。
當我們將這些組件,組合在一起使用時,建議進行集成測試,保證用戶行爲發生時,整個流程是能跑通的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章