今天的前端夜點心我們來聊聊在項目中單元測試應該測些什么?
以國內互聯網的開發節奏,在前端業務項目中全面覆蓋單元測試有時顯得不太可行,主要是因為以下這些絆腳石:
在這樣的處境下,一味強調單元測試的邏輯覆蓋率是沒有太大意義的,明確在哪里應用單測的能取得最大的邊際效益是更有意義的事情。
以下筆者根據自己的一些在單測的實戰經驗,列出了三項關于「單元測試應該測什么」的觀點并附以一些例子與大家交流:
拿來主義地對待單元測試
單測只是一種局部模塊測試,是諸多測試方案中的一種,認識到這一點可以避免我們為了測試而測試,或者為了指標而測試。
同時也應該認識到單測本身的覆蓋能力也是有限的,全部用例的 PASS 和 100% 的覆蓋率都不能保證被測試模塊的所有邏輯路徑都有正確的行為。
是否對一個模塊使用單元測試往往取決于這個模塊的邏輯穩定性和業務類型
例如對于一個底層 npm 包項目,單元測試幾乎是他唯一的代碼質量保障手段,這時就應該盡可能通過單元測試驗證它在各種應用場景下的行為是否符合預期,來最低成本地保證它每次發包和更新的質量。對這類項目,徹底應用 BDD 開發模式也會獲得越來越高的開發效率收益。
而對于一個功能復雜的 UI 組件,除了單元測試,還有 E2E 測試,自動化回歸測試,QA 手動測試(:blush:)來保障它的代碼質量。此時使用單元測試的邊際效益可能不是最高的,可以考慮通過別的手段來回歸它的邏輯。也可以考慮在初版功能驗證上線后通過快照測試(snapshot)來回歸驗證每一次迭代的邏輯。
讓模塊穿梭時空
單測的一個很重要的意義是幫助我們在開發階段模擬出 QA 手動測試(:blush:)甚至線上使用場景下都不易觸達的邊界場景,如:
等等
使用這類模擬對模塊進行單元測試的邊際效益是極高的,往往比 QA 去作等價的模擬快得多。
比如下面這段腳本,通過 jest 的 timer mock 能力,實現了對 expire
函數的測試:
const expire = (callback) => setTimeout(callback, 60000); // 一分鐘以后過期
test('到點就調用回調', () => {
const callback = jest.fn();
expire(callback);
jest.advanceTimersByTime(59999);
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(1);
expect(callback).toBeCalledOnce();
})
復制代碼
這段代碼通過 jest.advanceTimersByTime
精確模擬了宏任務的運行過程,同步完成了原本需要一分鐘才能驗證一次的異步流程的測試。
又比如下面的測試腳本用來測試一個名為 catchFromURL
的工具函數,該函數可以從當前的 URL 中獲取指定的參數作為返回值返回,同時從 URL 中抹去該參數。
這中需求通過 URL 攜帶 token 信息的業務場景(如單點登錄)中是非常常見的。
test('通過URL獲取指定的參數值并抹去之', () => {
const CURRENT_ORIGIN = document.location.origin;
const testHref = `${CURRENT_ORIGIN}/list/2/detail?a=123b&b=true#section2`;
history.replaceState(null, '', testHref);
expect(catchFromURL('a')).toBe('123b');
expect(document.location.href).toBe(`${CURRENT_ORIGIN}/list/2/detail?b=true#section2`);
})
復制代碼
這段測試代碼通過 jsdom 來實現對需要測試的環境的模擬。環境的構造和模擬其實是單元測試中的一個難點,由于 jsdom 本身的一些缺陷(如沒有實現 Navigator)使得在測試腳本運行的 node 環境中模擬正確的瀏覽器環境往往需要用到很多的 Hack 技術,這一點在未來的夜點心中會著重中展開討論。
less is more
測試代碼無需關心被測試模塊的具體實現,點到為止地測試幾種必要的流程場景即可。這一方面可以減少寫測試邏輯的時間,一方面可以使得業務邏輯具有更大的實現自由度。
對一個業務模塊,測試腳本只需要關心該模塊所關聯的所有外部性即可:
下面的腳本通過 enzyme
組件測試工具測試了一個名為 ValidatableInput
的 React 組件。這個組件在失焦(blur)時會觸發 onValidate
回調,并傳入 inputValue
參數。
test('失焦時觸發 onValidate', () => {
const onValidate = jest.mock();
const inputValue = '輸入的內容';
const wrapper = shallow(
<ValidatableInput
placeholder={''}
value={inputValue}
alert={''}
onChange={onChange}
onValidate={onValidate}
/>
);
wrapper.find('.validatable-input').first().simulate('blur');
expect(onValidate).toBeCalledWith(inputValue);
});
復制代碼
在上述測試用例中我們的測試邏輯完全基于行為開展,只關心失焦的「動作」和執行回調的「反饋」,沒有去斷言任何關于組件狀態的內容。
這樣組件可以根據它的需要自由地實現它的內部邏輯,例如添加通過外部的 Provider
來提供 value
和 onChange
成為受控組件的能力。這些實現的變化都不會影響當前這條測試用例的有效性。
上面就是一些對應該用單元測試測什么的看法,把單測用在它最擅長的地方,才能在緊湊的開發節奏中取得事半功倍的效果。