掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
作者 | 張霄翀

目前創(chuàng)新互聯(lián)建站已為上千多家的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬主機、網(wǎng)站托管運營、企業(yè)網(wǎng)站設(shè)計、龍江網(wǎng)站維護等服務(wù),公司將堅持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
我曾經(jīng)在好幾個項目里都近乎完整參與過補齊前端測試的工作,也收集到不同項目的同事很多關(guān)于前端測試的困惑和痛點,這其中大部分都很相似,我也感同身受,在這篇文章里,我會針對大家和自己常遇到的痛點分享一些自己的經(jīng)驗,如果你也有如下相似的困擾,那希望這篇文章能對你有些幫助~
常見問題(排名不分先后):
?
在分享問題的相關(guān)經(jīng)驗之前,我們先來梳理一下前端測試體系~
這其實跟所有測試的重要性是一樣的,大家有這么多的痛點也是因為知道覆蓋全面的測試可以對代碼質(zhì)量更有保證,讓我們更有信心地去重構(gòu)代碼,也能幫助我們更方便地了解現(xiàn)有的功能細節(jié),甚至是一些極端的邊界情況。而且在大家合作開發(fā)項目代碼的過程中,測試可以幫助我們更早地發(fā)現(xiàn)錯誤,減少時間成本,提高交付效率。
這兩個常見的測試方法論在這里簡單介紹一下,就不大篇幅展開了。TDD - (Test-Driven Development 測試驅(qū)動開發(fā))簡單地說就是先根據(jù)需求寫測試用例,然后實現(xiàn)代碼,通過后再接著寫下一個測試和實現(xiàn),循環(huán)直到全部功能和重構(gòu)完成?;舅悸肪褪峭ㄟ^測試來推動整個開發(fā)的進行。BDD - (Behavior Driven Development 行為驅(qū)動開發(fā)) 其實可以看做是TDD的一個分支。簡單地說就是先從外部定義業(yè)務(wù)行為,也就是測試用例,然后由外入內(nèi)的實現(xiàn)這些行為,最后得到的測試用例也是相應(yīng)業(yè)務(wù)行為的驗收標準。
在這里借一下前端大牛Kent C. Dodds的獎杯分層法來引出常見的分類:
(圖片出處:https://kentcdodds.com/blog/static-vs-unit-vs-integration-vs-e2e-tests)
端到端測試一般會運行在完整的應(yīng)用系統(tǒng)上(包括前端和后端),包含用戶完整的使用場景,比如打開瀏覽器,從注冊或登錄開始,在頁面內(nèi)導(dǎo)航,完成系統(tǒng)提供的功能,最后登出。
有時,我們也會在這里引入可視化用戶界面測試,即一種通過像素級比較屏幕截屏來驗證頁面顯示是否正確的測試。目的是確保界面在不同設(shè)備、瀏覽器、分辨率和操作系統(tǒng)下與預(yù)期的樣式一致??梢栽O(shè)置一定的偏差容忍值。這一層的測試成本較高,所以通常重心會放在確保主流程的功能正常上。常用工具:Cypress、Playwright、Puppeteer、TestCafe、Nightwatch (下載量對比)
集成測試 Integration Test
集成測試主要是測試當單元模塊組合到一起之后是否功能正常。在不同的測試上下文下可能有不同的定義,在前端測試這里通常指測試集成多個單元組件到一起的組件。
單元測試就是對沒有依賴或依賴都被mock掉了的測試單元的測試。在前端代碼里,它可能是:
主要是指利用一些代碼規(guī)范工具(Lint Tool)來及時捕獲代碼中潛在的語句錯誤,統(tǒng)一代碼格式等。這里就不展開了。常見工具和實踐有:
還是這張圖,我標記了一下:
在獎杯的形狀上每一層占的面積代表了應(yīng)該投入的重心比例。
這里集成測試的比重比單元測試大是因為集成測試可以在成本很高的e2e測試和離最終用戶行為較遠的單元測試之間取的一個平衡,它可以寫的很接近最終用戶的行為,成本又相對的沒那么高,屬于性價比很高的一部分。
所以集成測試有一些原則:
對于單元測試來說:
測試啟動工具負責將測試運行在Node.js或瀏覽器環(huán)境。形式可能是CLI或UI,并結(jié)合一定的配置。常見工具有:Jest / Karma / Jasmine / Cypress / TestCafe 等。
測試結(jié)構(gòu)工具提供一些方法和結(jié)構(gòu)將測試組織的更好,擁有更好的可讀性和可擴展性。如今,測試結(jié)構(gòu)通常以BDD形式來組織。測試結(jié)構(gòu)如下方Jest例子:
// Jest test structure
describe('calculator', () => {
// 第一層級: 標明測試的模塊名稱
beforeEach(() => {
// 每個測試之前都會跑,可以統(tǒng)一添加一些mock等
})
afterEach(() => {
// 每個測試之后都會跑,可以統(tǒng)一添加一些清理功能等
})
describe('add', () => {
// 第二層級: 標明測試的模塊功能分組
test('should add two numbers', () => {
// 實際的描述業(yè)務(wù)需求的測試
...
})
})
})
常見工具有:Jest / Mocha / Cucumber / Jasmine / Cypress / TestCafe 等。
斷言庫會提供一系列的方法來幫助驗證測試的結(jié)果是否符合預(yù)期。如下方的例子:
// Jest expect (popular)
expect(foo).toEqual('bar')
expect(foo).not.toBeNull()
// Chai expect
expect(foo).to.equal('bar')
expect(foo).to.not.be.null
常見工具有:Jest / Chai / Assert / TestCafe 等。
有的時候我們在測試的時候需要隔離一些代碼,模擬一些返回值,或監(jiān)控一些行為的調(diào)用次數(shù)和參數(shù),比如網(wǎng)絡(luò)請求的返回值,一些瀏覽器提供的功能,時間計時等,Mock工具會幫助我們更容易的去完成這些功能。
常見工具有:Sinon / Jest (spyOn, mock, useFakeTimers…) 等。
快照測試對于UI組件的渲染測試十分有效。原理是第一次運行時生成一張快照文件,需要開發(fā)人員確認快照的正確性,之后每一次運行測試都會生成一張快照并與之前的快照做比較,如果不匹配,則測試失敗。這時如果新的快照確實是更新代碼后的正確內(nèi)容,則可以更新之前保存的快照。(這里的快照通常都是框架渲染器生成的序列化后的字符串,而不是真實的圖片,這樣的測試效率比較高)。
這里可以參考Jest官方的用例。
常見工具有:Jest / Ava / Cypress
測試覆蓋率工具可以產(chǎn)出測試覆蓋率報告,通常會包含行、分支、函數(shù)、語句等各個維度的代碼覆蓋率,還可以生成可視化的html報告來可視化代碼覆蓋率。如以下的Jest內(nèi)置的代碼覆蓋率報告:
(圖片出處:??https://jestjs.io/)??
常見工具有:Jest內(nèi)置 / Istanbul。
上面在測試分層里介紹過的。
也在上面的測試分層里介紹過。通常會和e2e測試工具組合在一起使用,一般主流的e2e測試工具也會有對應(yīng)的庫去進行可視化用戶界面測試。
不同的前端框架還會有一些自帶的或推薦的測試庫,比如:
基于上面的分類,大家可能發(fā)現(xiàn)幾乎哪哪都有Jest,這類大而全的前端測試工具我們也可以稱為前端測試框架。
常見的有:
最后附上一張stateOfJS網(wǎng)站2021年的測試庫滿意度圖表供大家參考 :
(圖片出處:https://2021.stateofjs.com/en-US/libraries/testing/)
終于回到最開始的問題了,分享一下我的經(jīng)驗和通常的解決辦法:
前端測試感覺寫起來很復(fù)雜,會花很多時間,甚至經(jīng)常是業(yè)務(wù)代碼時間的好幾倍,這個問題可以分成三部分來下手:
可以根據(jù)剛才的測試策略部分,結(jié)合自己項目的實際情況,調(diào)整一下在不同的測試層分配的重心,定一下自己項目每個層級的測試粒度,這樣才能在保證交付的前提下達到測試信心值收益的最大化。
(1) 抽取公共的部分,使具體的測試文件簡潔
(2) 統(tǒng)一測試規(guī)范,有優(yōu)化及時重構(gòu)所有測試,這樣大家可以放心的參考已有測試,不會有多種寫法影響可讀性
// testUtils.js
export const flushPromises = (interval = 0) => {
return new Promise((resolve) => {
setTimeout(resolve, interval);
});
};
// example.test.js
test('should show ...', async () => {
//render component
await flushPromises();
//verify component
});
通常問這個問題背后隱藏的問題是前端很難先寫測試,再寫實現(xiàn)。確實我也有同感,如果是一些util/helper方法是可以很容易的遵循TDD的步驟的,但當涉及頁面結(jié)構(gòu)和樣式的時候,很難在寫測試的時候就想清楚頁面到底有哪些具體的元素,用到哪些需要mock的模塊。
所以在測試UI組件時,我通常會使用BDD的方式,具體步驟是:
// Jest
describe('todo component', () => {
test('should show todo list', () => {
// Snapshot test
const tree = renderer.create().toJSON();
expect(tree).toMatchSnapshot();
})
test.skip('should add todo when click add and input todo content', () => {
})
test.skip('should remove todo when click delete icon of todo item', () => {
})
當然隨著前端代碼寫的越來越熟練,為了提升效率,有時會簡化步驟,等一個小功能的組件都重構(gòu)完了,樣式調(diào)好了,所有的子組件都抽完了,再根據(jù)每個組件的props和交互的點批量加測試,最后用測試覆蓋率來驗證是否都覆蓋到了,保證自己新寫的組件都盡可能是100%的覆蓋率。
這個是我也很頭疼的問題,有的時候一些第三方組件因為要實現(xiàn)一些復(fù)雜的效果,會使用不一樣的方式去監(jiān)聽事件。
比如我們有一個Vue項目上用到了element-ui的select組件,這個組件可以通過:remote-method 屬性開啟異步發(fā)請求加載選項的功能,測試里想模擬異步拿到選項后并選擇某選項,就需要想辦法觸發(fā)它的@change 事件,通常一條await fireEvent.update(input, 'S'); 就搞定了,但這個怎么都不生效,仔細的查看它的實現(xiàn)才發(fā)現(xiàn)需要這么一串操作才能觸發(fā)到@change 事件。
const input = getByPlaceholderText('Please input to search');
await fireEvent.click(input);
await fireEvent.keyUp(input, { key: 'A', code: 'KeyA' });
await fireEvent.update(input, 'A');
await flushPromises(500); // 這個方法上面有介紹,的作用是讓異步的代碼返回結(jié)果,并且等待500ms,因為源碼有500ms的等待,這里就也需要等待
await fireEvent.click(getByText('Apple'));這里我總結(jié)的經(jīng)驗就是:
這個可以結(jié)合使用的測試工具去搜索,一般都會有很多現(xiàn)成的解決方案,在這里舉兩個例子:
Mock navigator.userAgent::
// jest.setup.js
Object.defineProperty(
global.navigator,
'userAgent',
((value) => ({
get() { return value; },
set(v) { value = v; },
}))(global.navigator['userAgent']),
);
// example.test.js
test('should show popup in Safari', () => {
global.navigator.userAgent = 'user agent of Safari ...';
// render and verify something
});
Mock window.open:
//jest.setup.js
Object.defineProperty(
window,
'open',
((value) => ({
get() { return value; },
set(v) { value = v; },
}))(window.open),
);
// example.test.js
test('should ...', () => {
window.open = jest.fn();
// render something
expect(window.open).toBeCalledWith('xxx', '_blank');
});
上面有介紹,可以將公共的部分抽取出去,又能減少代碼重復(fù),又能提升寫測試的效率,比如準備數(shù)據(jù)的部分可以抽成公共的fixture文件,提供方法生成默認的數(shù)據(jù),也可以通過參數(shù)去覆蓋修改部分數(shù)據(jù),達到定制化的目的:
export const generateUser = (user = {}) => {
return {
id: 1,
firstName: 'San',
lastName: 'Zhang',
email: '[email protected]',
...user,
};
};
測試里的報錯通常都很有價值,需要重視。這里面的錯誤有可能是:
雖然有的時候也會有一些由于第三方庫的原因引起的無法修復(fù)又沒有影響的log,可以忽略,但測試里大部分警告Log其實都是可以修復(fù)的,甚至在修復(fù)后可能得到意想不到的受益,比如發(fā)現(xiàn)真正業(yè)務(wù)代碼的問題,測試不再隨機掛了,測試運行性能提升了等等。
對于前端測試,我覺得重心不是機械的去追求測試覆蓋率,而是盡可能的在成本和信心值中間找到一個平衡,應(yīng)用一些好的實踐去降低寫測試的成本,提升寫測試帶來的回報,讓大家對于項目質(zhì)量越來越有信心。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流