国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專(zhuān)欄INFORMATION COLUMN

Web 前端單元測(cè)試到底要怎么寫(xiě)?看這一篇就夠了

lastSeries / 3483人閱讀

摘要:隨著應(yīng)用的復(fù)雜程度越來(lái)越高,很多公司越來(lái)越重視前端單元測(cè)試。最后我們可以利用覆蓋率來(lái)看下用例的覆蓋程度是否足夠一般來(lái)說(shuō)不用刻意追求,根據(jù)實(shí)際情況來(lái)定單元測(cè)試是測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的基礎(chǔ)。

隨著 Web 應(yīng)用的復(fù)雜程度越來(lái)越高,很多公司越來(lái)越重視前端單元測(cè)試。我們看到的大多數(shù)教程都會(huì)講單元測(cè)試的重要性、一些有代表性的測(cè)試框架 api 怎么使用,但在實(shí)際項(xiàng)目中單元測(cè)試要怎么下手?測(cè)試用例應(yīng)該包含哪些具體內(nèi)容呢?

本文從一個(gè)真實(shí)的應(yīng)用場(chǎng)景出發(fā),從設(shè)計(jì)模式、代碼結(jié)構(gòu)來(lái)分析單元測(cè)試應(yīng)該包含哪些內(nèi)容,具體測(cè)試用例怎么寫(xiě),希望看到的童鞋都能有所收獲。
完整的代碼內(nèi)容在 這里 (各位童鞋覺(jué)得好幫忙去給個(gè) 哈)。

項(xiàng)目用到的技術(shù)框架

該項(xiàng)目采用 react 技術(shù)棧,用到的主要框架包括:reactreduxreact-reduxredux-actions、reselect、redux-saga、seamless-immutableantd。

應(yīng)用場(chǎng)景介紹

這個(gè)應(yīng)用場(chǎng)景從 UI 層來(lái)講主要由兩個(gè)部分組成:

工具欄,包含刷新按鈕、關(guān)鍵字搜索框

表格展示,采用分頁(yè)的形式瀏覽

看到這里有的童鞋可能會(huì)說(shuō):切!這么簡(jiǎn)單的界面和業(yè)務(wù)邏輯,還是真實(shí)場(chǎng)景嗎,還需要寫(xiě)神馬單元測(cè)試嗎?

別急,為了保證文章的閱讀體驗(yàn)和長(zhǎng)度適中,能講清楚問(wèn)題的簡(jiǎn)潔場(chǎng)景就是好場(chǎng)景不是嗎?慢慢往下看。

設(shè)計(jì)模式與結(jié)構(gòu)分析

在這個(gè)場(chǎng)景設(shè)計(jì)開(kāi)發(fā)中,我們嚴(yán)格遵守 redux 單向數(shù)據(jù)流 與 react-redux 的最佳實(shí)踐,并采用 redux-saga 來(lái)處理業(yè)務(wù)流,reselect 來(lái)處理狀態(tài)緩存,通過(guò) fetch 來(lái)調(diào)用后臺(tái)接口,與真實(shí)的項(xiàng)目沒(méi)有差異。

分層設(shè)計(jì)與代碼組織如下所示:

中間 store 中的內(nèi)容都是 redux 相關(guān)的,看名稱(chēng)應(yīng)該都能知道意思了。

具體的代碼請(qǐng)看 這里。

單元測(cè)試部分介紹

先講一下用到了哪些測(cè)試框架和工具,主要內(nèi)容包括:

jest ,測(cè)試框架

enzyme ,專(zhuān)測(cè) react ui 層

sinon ,具有獨(dú)立的 fakes、spies、stubs、mocks 功能庫(kù)

nock ,模擬 HTTP Server

如果有童鞋對(duì)上面這些使用和配置不熟的話,直接看官方文檔吧,比任何教程都寫(xiě)的好。

接下來(lái),我們就開(kāi)始編寫(xiě)具體的測(cè)試用例代碼了,下面會(huì)針對(duì)每個(gè)層面給出代碼片段和解析。那么我們先從 actions 開(kāi)始吧。

為使文章盡量簡(jiǎn)短、清晰,下面的代碼片段不是每個(gè)文件的完整內(nèi)容,完整內(nèi)容在 這里 。
actions

業(yè)務(wù)里面我使用了 redux-actions 來(lái)產(chǎn)生 action,這里用工具欄做示例,先看一段業(yè)務(wù)代碼:

import { createAction } from "redux-actions";
import * as type from "../types/bizToolbar";

export const updateKeywords = createAction(type.BIZ_TOOLBAR_KEYWORDS_UPDATE);

// ...

對(duì)于 actions 測(cè)試,我們主要是驗(yàn)證產(chǎn)生的 action 對(duì)象是否正確:

import * as type from "@/store/types/bizToolbar";
import * as actions from "@/store/actions/bizToolbar";

/* 測(cè)試 bizToolbar 相關(guān) actions */
describe("bizToolbar actions", () => {
  
    /* 測(cè)試更新搜索關(guān)鍵字 */
    test("should create an action for update keywords", () => {
        // 構(gòu)建目標(biāo) action
        const keywords = "some keywords";
        const expectedAction = {
            type: type.BIZ_TOOLBAR_KEYWORDS_UPDATE,
            payload: keywords
        };

        // 斷言 redux-actions 產(chǎn)生的 action 是否正確
        expect(actions.updateKeywords(keywords)).toEqual(expectedAction);
    });

    // ...
});

這個(gè)測(cè)試用例的邏輯很簡(jiǎn)單,首先構(gòu)建一個(gè)我們期望的結(jié)果,然后調(diào)用業(yè)務(wù)代碼,最后驗(yàn)證業(yè)務(wù)代碼的運(yùn)行結(jié)果與期望是否一致。這就是寫(xiě)測(cè)試用例的基本套路。

我們?cè)趯?xiě)測(cè)試用例時(shí)盡量保持用例的單一職責(zé),不要覆蓋太多不同的業(yè)務(wù)范圍。測(cè)試用例數(shù)量可以有很多個(gè),但每個(gè)都不應(yīng)該很復(fù)雜。

reducers

接著是 reducers,依然采用 redux-actionshandleActions 來(lái)編寫(xiě) reducer,這里用表格的來(lái)做示例:

import { handleActions } from "redux-actions";
import Immutable from "seamless-immutable";
import * as type from "../types/bizTable";

/* 默認(rèn)狀態(tài) */
export const defaultState = Immutable({
    loading: false,
    pagination: {
        current: 1,
        pageSize: 15,
        total: 0
    },
    data: []
});

export default handleActions(
    {
        // ...

        /* 處理獲得數(shù)據(jù)成功 */
        [type.BIZ_TABLE_GET_RES_SUCCESS]: (state, {payload}) => {
            return state.merge(
                {
                    loading: false,
                    pagination: {total: payload.total},
                    data: payload.items
                },
                {deep: true}
            );
        },
        
        // ...
    },
    defaultState
);
這里的狀態(tài)對(duì)象使用了 seamless-immutable

對(duì)于 reducer,我們主要測(cè)試兩個(gè)方面:

對(duì)于未知的 action.type ,是否能返回當(dāng)前狀態(tài)。

對(duì)于每個(gè)業(yè)務(wù) type ,是否都返回了經(jīng)過(guò)正確處理的狀態(tài)。

下面是針對(duì)以上兩點(diǎn)的測(cè)試代碼:

import * as type from "@/store/types/bizTable";
import reducer, { defaultState } from "@/store/reducers/bizTable";

/* 測(cè)試 bizTable reducer */
describe("bizTable reducer", () => {
    
    /* 測(cè)試未指定 state 參數(shù)情況下返回當(dāng)前缺省 state */
    test("should return the default state", () => {
        expect(reducer(undefined, {type: "UNKNOWN"})).toEqual(defaultState);
    });
    
    // ...
    
    /* 測(cè)試處理正常數(shù)據(jù)結(jié)果 */
    test("should handle successful data response", () => {
        /* 模擬返回?cái)?shù)據(jù)結(jié)果 */
        const payload = {
            items: [
                {id: 1, code: "1"},
                {id: 2, code: "2"}
            ],
            total: 2
        };
        /* 期望返回的狀態(tài) */
        const expectedState = defaultState
            .setIn(["pagination", "total"], payload.total)
            .set("data", payload.items)
            .set("loading", false);

        expect(
            reducer(defaultState, {
                type: type.BIZ_TABLE_GET_RES_SUCCESS,
                payload
            })
        ).toEqual(expectedState);
    });
    
    // ...
});

這里的測(cè)試用例邏輯也很簡(jiǎn)單,依然是上面斷言期望結(jié)果的套路。下面是 selectors 的部分。

selectors

selector 的作用是獲取對(duì)應(yīng)業(yè)務(wù)的狀態(tài),這里使用了 reselect 來(lái)做緩存,防止 state 未改變的情況下重新計(jì)算,先看一下表格的 selector 代碼:

import { createSelector } from "reselect";
import * as defaultSettings from "@/utils/defaultSettingsUtil";

// ...

const getBizTableState = (state) => state.bizTable;

export const getBizTable = createSelector(getBizTableState, (bizTable) => {
    return bizTable.merge({
        pagination: defaultSettings.pagination
    }, {deep: true});
});

這里的分頁(yè)器部分參數(shù)在項(xiàng)目中是統(tǒng)一設(shè)置,所以 reselect 很好的完成了這個(gè)工作:如果業(yè)務(wù)狀態(tài)不變,直接返回上次的緩存。分頁(yè)器默認(rèn)設(shè)置如下:

export const pagination = {
    size: "small",
    showTotal: (total, range) => `${range[0]}-${range[1]} / ${total}`,
    pageSizeOptions: ["15", "25", "40", "60"],
    showSizeChanger: true,
    showQuickJumper: true
};

那么我們的測(cè)試也主要是兩個(gè)方面:

對(duì)于業(yè)務(wù) selector ,是否返回了正確的內(nèi)容。

緩存功能是否正常。

測(cè)試代碼如下:

import Immutable from "seamless-immutable";
import { getBizTable } from "@/store/selectors";
import * as defaultSettingsUtil from "@/utils/defaultSettingsUtil";

/* 測(cè)試 bizTable selector */
describe("bizTable selector", () => {
    
    let state;

    beforeEach(() => {
        state = createState();
        /* 每個(gè)用例執(zhí)行前重置緩存計(jì)算次數(shù) */
        getBizTable.resetRecomputations();
    });

    function createState() {
        return Immutable({
            bizTable: {
                loading: false,
                pagination: {
                    current: 1,
                    pageSize: 15,
                    total: 0
                },
                data: []
            }
        });
    }

    /* 測(cè)試返回正確的 bizTable state */
    test("should return bizTable state", () => {
        /* 業(yè)務(wù)狀態(tài) ok 的 */
        expect(getBizTable(state)).toMatchObject(state.bizTable);
        
        /* 分頁(yè)默認(rèn)參數(shù)設(shè)置 ok 的 */
        expect(getBizTable(state)).toMatchObject({
            pagination: defaultSettingsUtil.pagination
        });
    });

    /* 測(cè)試 selector 緩存是否有效 */
    test("check memoization", () => {
        getBizTable(state);
        /* 第一次計(jì)算,緩存計(jì)算次數(shù)為 1 */
        expect(getBizTable.recomputations()).toBe(1);
        
        getBizTable(state);
        /* 業(yè)務(wù)狀態(tài)不變的情況下,緩存計(jì)算次數(shù)應(yīng)該還是 1 */
        expect(getBizTable.recomputations()).toBe(1);
        
        const newState = state.setIn(["bizTable", "loading"], true);
        getBizTable(newState);
        /* 業(yè)務(wù)狀態(tài)改變了,緩存計(jì)算次數(shù)應(yīng)該是 2 了 */
        expect(getBizTable.recomputations()).toBe(2);
    });
});

測(cè)試用例依然很簡(jiǎn)單有木有?保持這個(gè)節(jié)奏就對(duì)了。下面來(lái)講下稍微有點(diǎn)復(fù)雜的地方,sagas 部分。

sagas

這里我用了 redux-saga 處理業(yè)務(wù)流,這里具體也就是異步調(diào)用 api 請(qǐng)求數(shù)據(jù),處理成功結(jié)果和錯(cuò)誤結(jié)果等。

可能有的童鞋覺(jué)得搞這么復(fù)雜干嘛,異步請(qǐng)求用個(gè) redux-thunk 不就完事了嗎?別急,耐心看完你就明白了。

這里有必要大概介紹下 redux-saga 的工作方式。saga 是一種 es6 的生成器函數(shù) - Generator ,我們利用他來(lái)產(chǎn)生各種聲明式的 effects ,由 redux-saga 引擎來(lái)消化處理,推動(dòng)業(yè)務(wù)進(jìn)行。

這里我們來(lái)看看獲取表格數(shù)據(jù)的業(yè)務(wù)代碼:

import { all, takeLatest, put, select, call } from "redux-saga/effects";
import * as type from "../types/bizTable";
import * as actions from "../actions/bizTable";
import { getBizToolbar, getBizTable } from "../selectors";
import * as api from "@/services/bizApi";

// ...

export function* onGetBizTableData() {
    /* 先獲取 api 調(diào)用需要的參數(shù):關(guān)鍵字、分頁(yè)信息等 */
    const {keywords} = yield select(getBizToolbar);
    const {pagination} = yield select(getBizTable);

    const payload = {
        keywords,
        paging: {
            skip: (pagination.current - 1) * pagination.pageSize, max: pagination.pageSize
        }
    };

    try {
        /* 調(diào)用 api */
        const result = yield call(api.getBizTableData, payload);
        /* 正常返回 */
        yield put(actions.putBizTableDataSuccessResult(result));
    } catch (err) {
        /* 錯(cuò)誤返回 */
        yield put(actions.putBizTableDataFailResult());
    }
}

不熟悉 redux-saga 的童鞋也不要太在意代碼的具體寫(xiě)法,看注釋?xiě)?yīng)該能了解這個(gè)業(yè)務(wù)的具體步驟:

從對(duì)應(yīng)的 state 里取到調(diào)用 api 時(shí)需要的參數(shù)部分(搜索關(guān)鍵字、分頁(yè)),這里調(diào)用了剛才的 selector。

組合好參數(shù)并調(diào)用對(duì)應(yīng)的 api 層。

如果正常返回結(jié)果,則發(fā)送成功 action 通知 reducer 更新?tīng)顟B(tài)。

如果錯(cuò)誤返回,則發(fā)送錯(cuò)誤 action 通知 reducer。

那么具體的測(cè)試用例應(yīng)該怎么寫(xiě)呢?我們都知道這種業(yè)務(wù)代碼涉及到了 api 或其他層的調(diào)用,如果要寫(xiě)單元測(cè)試必須做一些 mock 之類(lèi)來(lái)防止真正調(diào)用 api 層,下面我們來(lái)看一下 怎么針對(duì)這個(gè) saga 來(lái)寫(xiě)測(cè)試用例:

import { put, select } from "redux-saga/effects";

// ...

/* 測(cè)試獲取數(shù)據(jù) */
test("request data, check success and fail", () => {
    /* 當(dāng)前的業(yè)務(wù)狀態(tài) */
    const state = {
        bizToolbar: {
            keywords: "some keywords"
        },
        bizTable: {
            pagination: {
                current: 1,
                pageSize: 15
            }
        }
    };
    const gen = cloneableGenerator(saga.onGetBizTableData)();

    /* 1. 是否調(diào)用了正確的 selector 來(lái)獲得請(qǐng)求時(shí)要發(fā)送的參數(shù) */
    expect(gen.next().value).toEqual(select(getBizToolbar));
    expect(gen.next(state.bizToolbar).value).toEqual(select(getBizTable));

    /* 2. 是否調(diào)用了 api 層 */
    const callEffect = gen.next(state.bizTable).value;
    expect(callEffect["CALL"].fn).toBe(api.getBizTableData);
    /* 調(diào)用 api 層參數(shù)是否傳遞正確 */
    expect(callEffect["CALL"].args[0]).toEqual({
        keywords: "some keywords",
        paging: {skip: 0, max: 15}
    });

    /* 3. 模擬正確返回分支 */
    const successBranch = gen.clone();
    const successRes = {
        items: [
            {id: 1, code: "1"},
            {id: 2, code: "2"}
        ],
        total: 2
    };
    expect(successBranch.next(successRes).value).toEqual(
        put(actions.putBizTableDataSuccessResult(successRes)));
    expect(successBranch.next().done).toBe(true);

    /* 4. 模擬錯(cuò)誤返回分支 */
    const failBranch = gen.clone();
    expect(failBranch.throw(new Error("模擬產(chǎn)生異常")).value).toEqual(
        put(actions.putBizTableDataFailResult()));
    expect(failBranch.next().done).toBe(true);
});

這個(gè)測(cè)試用例相比前面的復(fù)雜了一些,我們先來(lái)說(shuō)下測(cè)試 saga 的原理。前面說(shuō)過(guò) saga 實(shí)際上是返回各種聲明式的 effects ,然后由引擎來(lái)真正執(zhí)行。所以我們測(cè)試的目的就是要看 effects 的產(chǎn)生是否符合預(yù)期。那么effect 到底是個(gè)神馬東西呢?其實(shí)就是字面量對(duì)象!

我們可以用在業(yè)務(wù)代碼同樣的方式來(lái)產(chǎn)生這些字面量對(duì)象,對(duì)于字面量對(duì)象的斷言就非常簡(jiǎn)單了,并且沒(méi)有直接調(diào)用 api 層,就用不著做 mock 咯!這個(gè)測(cè)試用例的步驟就是利用生成器函數(shù)一步步的產(chǎn)生下一個(gè) effect ,然后斷言比較。

從上面的注釋 3、4 可以看到,redux-saga 還提供了一些輔助函數(shù)來(lái)方便的處理分支斷點(diǎn)。

這也是我選擇 redux-saga 的原因:強(qiáng)大并且利于測(cè)試。

api 和 fetch 工具庫(kù)

接下來(lái)就是api 層相關(guān)的了。前面講過(guò)調(diào)用后臺(tái)請(qǐng)求是用的 fetch ,我封裝了兩個(gè)方法來(lái)簡(jiǎn)化調(diào)用和結(jié)果處理:getJSON()postJSON() ,分別對(duì)應(yīng) GET 、POST 請(qǐng)求。先來(lái)看看 api 層代碼:

import { fetcher } from "@/utils/fetcher";

export function getBizTableData(payload) {
    return fetcher.postJSON("/api/biz/get-table", payload);
}

業(yè)務(wù)代碼很簡(jiǎn)單,那么測(cè)試用例也很簡(jiǎn)單:

import sinon from "sinon";
import { fetcher } from "@/utils/fetcher";
import * as api from "@/services/bizApi";

/* 測(cè)試 bizApi */
describe("bizApi", () => {
    
    let fetcherStub;

    beforeAll(() => {
        fetcherStub = sinon.stub(fetcher);
    });

    // ...

    /* getBizTableData api 應(yīng)該調(diào)用正確的 method 和傳遞正確的參數(shù) */
    test("getBizTableData api should call postJSON with right params of fetcher", () => {
        /* 模擬參數(shù) */
        const payload = {a: 1, b: 2};
        api.getBizTableData(payload);

        /* 檢查是否調(diào)用了工具庫(kù) */
        expect(fetcherStub.postJSON.callCount).toBe(1);
        /* 檢查調(diào)用參數(shù)是否正確 */
        expect(fetcherStub.postJSON.lastCall.calledWith("/api/biz/get-table", payload)).toBe(true);
    });
});

由于 api 層直接調(diào)用了工具庫(kù),所以這里用 sinon.stub() 來(lái)替換工具庫(kù)達(dá)到測(cè)試目的。

接著就是測(cè)試自己封裝的 fetch 工具庫(kù)了,這里 fetch 我是用的 isomorphic-fetch ,所以選擇了 nock 來(lái)模擬 Server 進(jìn)行測(cè)試,主要是測(cè)試正常訪問(wèn)返回結(jié)果和模擬服務(wù)器異常等,示例片段如下:

import nock from "nock";
import { fetcher, FetchError } from "@/utils/fetcher";

/* 測(cè)試 fetcher */
describe("fetcher", () => {

    afterEach(() => {
        nock.cleanAll();
    });

    afterAll(() => {
        nock.restore();
    });

    /* 測(cè)試 getJSON 獲得正常數(shù)據(jù) */
    test("should get success result", () => {
        nock("http://some")
            .get("/test")
            .reply(200, {success: true, result: "hello, world"});

        return expect(fetcher.getJSON("http://some/test")).resolves.toMatch(/^hello.+$/);
    });

    // ...

    /* 測(cè)試 getJSON 捕獲 server 大于 400 的異常狀態(tài) */
    test("should catch server status: 400+", (done) => {
        const status = 500;
        nock("http://some")
            .get("/test")
            .reply(status);

        fetcher.getJSON("http://some/test").catch((error) => {
            expect(error).toEqual(expect.any(FetchError));
            expect(error).toHaveProperty("detail");
            expect(error.detail.status).toBe(status);
            done();
        });
    });

   /* 測(cè)試 getJSON 傳遞正確的 headers 和 query strings */
    test("check headers and query string of getJSON()", () => {
        nock("http://some", {
            reqheaders: {
                "Accept": "application/json",
                "authorization": "Basic Auth"
            }
        })
            .get("/test")
            .query({a: "123", b: 456})
            .reply(200, {success: true, result: true});

        const headers = new Headers();
        headers.append("authorization", "Basic Auth");
        return expect(fetcher.getJSON(
            "http://some/test", {a: "123", b: 456}, headers)).resolves.toBe(true);
    });
    
    // ...
});

基本也沒(méi)什么復(fù)雜的,主要注意 fetch 是 promise 返回,jest 的各種異步測(cè)試方案都能很好滿(mǎn)足。

剩下的部分就是跟 UI 相關(guān)的了。

容器組件

容器組件的主要目的是傳遞 state 和 actions,看下工具欄的容器組件代碼:

import { connect } from "react-redux";
import { getBizToolbar } from "@/store/selectors";
import * as actions from "@/store/actions/bizToolbar";
import BizToolbar from "@/components/BizToolbar";

const mapStateToProps = (state) => ({
    ...getBizToolbar(state)
});

const mapDispatchToProps = {
    reload: actions.reload,
    updateKeywords: actions.updateKeywords
};

export default connect(mapStateToProps, mapDispatchToProps)(BizToolbar);

那么測(cè)試用例的目的也是檢查這些,這里使用了 redux-mock-store 來(lái)模擬 redux 的 store :

import React from "react";
import { shallow } from "enzyme";
import configureStore from "redux-mock-store";
import BizToolbar from "@/containers/BizToolbar";

/* 測(cè)試容器組件 BizToolbar */
describe("BizToolbar container", () => {
    
    const initialState = {
        bizToolbar: {
            keywords: "some keywords"
        }
    };
    const mockStore = configureStore();
    let store;
    let container;

    beforeEach(() => {
        store = mockStore(initialState);
        container = shallow();
    });

    /* 測(cè)試 state 到 props 的映射是否正確 */
    test("should pass state to props", () => {
        const props = container.props();

        expect(props).toHaveProperty("keywords", initialState.bizToolbar.keywords);
    });

    /* 測(cè)試 actions 到 props 的映射是否正確 */
    test("should pass actions to props", () => {
        const props = container.props();

        expect(props).toHaveProperty("reload", expect.any(Function));
        expect(props).toHaveProperty("updateKeywords", expect.any(Function));
    });
});

很簡(jiǎn)單有木有,所以也沒(méi)啥可說(shuō)的了。

UI 組件

這里以表格組件作為示例,我們將直接來(lái)看測(cè)試用例是怎么寫(xiě)。一般來(lái)說(shuō) UI 組件我們主要測(cè)試以下幾個(gè)方面:

是否渲染了正確的 DOM 結(jié)構(gòu)

樣式是否正確

業(yè)務(wù)邏輯觸發(fā)是否正確

下面是測(cè)試用例代碼:

import React from "react";
import { mount } from "enzyme";
import sinon from "sinon";
import { Table } from "antd";
import * as defaultSettingsUtil from "@/utils/defaultSettingsUtil";
import BizTable from "@/components/BizTable";

/* 測(cè)試 UI 組件 BizTable */
describe("BizTable component", () => {
    
    const defaultProps = {
        loading: false,
        pagination: Object.assign({}, {
            current: 1,
            pageSize: 15,
            total: 2
        }, defaultSettingsUtil.pagination),
        data: [{id: 1}, {id: 2}],
        getData: sinon.fake(),
        updateParams: sinon.fake()
    };
    let defaultWrapper;

    beforeEach(() => {
        defaultWrapper = mount();
    });

    // ...

    /* 測(cè)試是否渲染了正確的功能子組件 */
    test("should render table and pagination", () => {
        /* 是否渲染了 Table 組件 */
        expect(defaultWrapper.find(Table).exists()).toBe(true);
        /* 是否渲染了 分頁(yè)器 組件,樣式是否正確(mini) */
        expect(defaultWrapper.find(".ant-table-pagination.mini").exists()).toBe(true);
    });

    /* 測(cè)試首次加載時(shí)數(shù)據(jù)列表為空是否發(fā)起加載數(shù)據(jù)請(qǐng)求 */
    test("when componentDidMount and data is empty, should getData", () => {
        sinon.spy(BizTable.prototype, "componentDidMount");
        const props = Object.assign({}, defaultProps, {
            pagination: Object.assign({}, {
                current: 1,
                pageSize: 15,
                total: 0
            }, defaultSettingsUtil.pagination),
            data: []
        });
        const wrapper = mount();

        expect(BizTable.prototype.componentDidMount.calledOnce).toBe(true);
        expect(props.getData.calledOnce).toBe(true);
        BizTable.prototype.componentDidMount.restore();
    });

    /* 測(cè)試 table 翻頁(yè)后是否正確觸發(fā) updateParams */
    test("when change pagination of table, should updateParams", () => {
        const table = defaultWrapper.find(Table);
        table.props().onChange({current: 2, pageSize: 25});
        expect(defaultProps.updateParams.lastCall.args[0])
            .toEqual({paging: {current: 2, pageSize: 25}});
    });
});

得益于設(shè)計(jì)分層的合理性,我們很容易利用構(gòu)造 props 來(lái)達(dá)到測(cè)試目的,結(jié)合 enzymesinon ,測(cè)試用例依然保持簡(jiǎn)單的節(jié)奏。

總結(jié)

以上就是這個(gè)場(chǎng)景完整的測(cè)試用例編寫(xiě)思路和示例代碼,文中提及的思路方法也完全可以用在 VueAngular 項(xiàng)目上。完整的代碼內(nèi)容在 這里 (重要的事情多說(shuō)幾遍,各位童鞋覺(jué)得好幫忙去給個(gè) 哈)。

最后我們可以利用覆蓋率來(lái)看下用例的覆蓋程度是否足夠(一般來(lái)說(shuō)不用刻意追求 100%,根據(jù)實(shí)際情況來(lái)定):

單元測(cè)試是 TDD 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的基礎(chǔ)。從以上整個(gè)過(guò)程可以看出,好的設(shè)計(jì)分層是很容易編寫(xiě)測(cè)試用例的,單元測(cè)試不單單只是為了保證代碼質(zhì)量:他會(huì)逼著你思考代碼設(shè)計(jì)的合理性,拒絕面條代碼

借用 Clean Code 的結(jié)束語(yǔ):

2005 年,在參加于丹佛舉行的敏捷大會(huì)時(shí),Elisabeth Hedrickson 遞給我一條類(lèi)似 Lance Armstrong 熱銷(xiāo)的那種綠色腕帶。這條腕帶上面寫(xiě)著“沉迷測(cè)試”(Test Obsessed)的字樣。我高興地戴上,并自豪地一直系著。自從 1999 年從 Kent Beck 那兒學(xué)到 TDD 以來(lái),我的確迷上了測(cè)試驅(qū)動(dòng)開(kāi)發(fā)。

不過(guò)跟著就發(fā)生了些奇事。我發(fā)現(xiàn)自己無(wú)法取下腕帶。不僅是因?yàn)橥髱Ш芫o,而且那也是條精神上的緊箍咒。那腕帶就是我職業(yè)道德的宣告,也是我承諾盡己所能寫(xiě)出最好代碼的提示。取下它,仿佛就是違背了這些宣告和承諾似的。

所以它還在我的手腕上。在寫(xiě)代碼時(shí),我用余光瞟見(jiàn)它。它一直提醒我,我做了寫(xiě)出整潔代碼的承諾。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/96720.html

相關(guān)文章

  • 過(guò)濾器入門(mén)看這一篇夠了

    摘要:我們很容易發(fā)現(xiàn),過(guò)濾器可以比喻成一張濾網(wǎng)。這究竟是怎么回事啊我們可以這樣理解過(guò)濾器不單單只有一個(gè),那么我們?cè)趺垂芾磉@些過(guò)濾器呢在中就使用了鏈?zhǔn)浇Y(jié)構(gòu)。第一種方式在文件中配置用于注冊(cè)過(guò)濾器用于為過(guò)濾器指定一個(gè)名字,該元素的內(nèi)容不能為空。 什么是過(guò)濾器 過(guò)濾器是Servlet的高級(jí)特性之一,也別把它想得那么高深,只不過(guò)是實(shí)現(xiàn)Filter接口的Java類(lèi)罷了! 首先,我們來(lái)看看過(guò)濾器究竟Web...

    yy13818512006 評(píng)論0 收藏0
  • 你真的完全了解Java動(dòng)態(tài)代理嗎?看這夠了

    摘要:動(dòng)態(tài)地代理,可以猜測(cè)一下它的含義,在運(yùn)行時(shí)動(dòng)態(tài)地對(duì)某些東西代理,代理它做了其他事情。所以動(dòng)態(tài)代理的內(nèi)容重點(diǎn)就是這個(gè)。所以下一篇我們來(lái)細(xì)致了解下的到底是怎么使用動(dòng)態(tài)代理的。 之前講了《零基礎(chǔ)帶你看Spring源碼——IOC控制反轉(zhuǎn)》,本來(lái)打算下一篇講講Srping的AOP的,但是其中會(huì)涉及到Java的動(dòng)態(tài)代理,所以先單獨(dú)一篇來(lái)了解下Java的動(dòng)態(tài)代理到底是什么,Java是怎么實(shí)現(xiàn)它的。 ...

    haitiancoder 評(píng)論0 收藏0
  • Spring入門(mén)看這一篇夠了

    摘要:甲乙交易活動(dòng)不需要雙方見(jiàn)面,避免了雙方的互不信任造成交易失敗的問(wèn)題。這就是的核心思想。統(tǒng)一配置,便于修改。帶參數(shù)的構(gòu)造函數(shù)創(chuàng)建對(duì)象首先,就要提供帶參數(shù)的構(gòu)造函數(shù)接下來(lái),關(guān)鍵是怎么配置文件了。 前言 前面已經(jīng)學(xué)習(xí)了Struts2和Hibernate框架了。接下來(lái)學(xué)習(xí)的是Spring框架...本博文主要是引入Spring框架... Spring介紹 Spring誕生: 創(chuàng)建Spring的...

    superw 評(píng)論0 收藏0
  • 瀏覽器緩存看這一篇夠了

    摘要:瀏覽器緩存作為性能優(yōu)化的重要一環(huán),對(duì)于前端而言,重要性不言而喻。根據(jù)瀏覽器發(fā)送的修改時(shí)間和服務(wù)端的修改時(shí)間進(jìn)行比對(duì),一致的話代表資源沒(méi)有改變,服務(wù)端返回正文為空的響應(yīng),讓瀏覽器中緩存中讀取資源,這就大大減小了請(qǐng)求的消耗。 瀏覽器緩存作為性能優(yōu)化的重要一環(huán),對(duì)于前端而言,重要性不言而喻。以前總是一知半解的,所以這次好好整理總結(jié)了一下。 1、緩存機(jī)制 首先我們來(lái)總體感知一下它的匹配流程,如...

    tinysun1234 評(píng)論0 收藏0
  • Scrapy詳解 爬蟲(chóng)框架入門(mén)看這一篇夠了!

    摘要:目錄前言架構(gòu)安裝第一個(gè)爬蟲(chóng)爬取有道翻譯創(chuàng)建項(xiàng)目創(chuàng)建創(chuàng)建解析運(yùn)行爬蟲(chóng)爬取單詞釋義下載單詞語(yǔ)音文件前言學(xué)習(xí)有一段時(shí)間了,當(dāng)時(shí)想要獲取一下百度漢字的解析,又不想一個(gè)個(gè)漢字去搜,復(fù)制粘貼太費(fèi)勁,考慮到爬蟲(chóng)的便利性,這篇文章是介紹一個(gè)爬蟲(chóng)框架, 目錄 前言 架構(gòu) 安裝 第一個(gè)爬蟲(chóng):爬取有道翻譯 創(chuàng)建項(xiàng)目 創(chuàng)建Item 創(chuàng)建Spider 解析 運(yùn)行爬蟲(chóng)-爬取單詞釋義 下載單詞語(yǔ)音文件 ...

    lordharrd 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<