您好,登錄后才能下訂單哦!
這篇文章主要介紹“web前端單元測試之UI測試功能性代碼實例分析”,在日常操作中,相信很多人在web前端單元測試之UI測試功能性代碼實例分析問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”web前端單元測試之UI測試功能性代碼實例分析”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
很容易理解,前端的項目終究還是會展示在瀏覽器上,那么所謂的UI測試也就是測試我們寫的代碼和預期渲染的UI是否一致而已。
結合到我們現在實際項目,多數基于UI框架的情況(比如React),我們只需要跟隨官方提供的UI測試途徑即可,千萬別去直接搜索XX前端單元測試框架之類的。每個框架都有自己的特性,或多或少都會涉及到UI的渲染,比如虛擬DOM、生命周期之類。所以像React
官方文檔提到的@testing-library/react
(也有曾經使用過比較優秀的Enzyme
),都會根據React
框架特性提供相關的API例如:render、renderHook
之類的。
簡單的說就是一段封裝的代碼,無論是一個函數或者是一個類。而這段代碼多數情況和UI無關,我們只需要要關注功能實現的本身,最簡單的就是一個函數,輸入什么參數,返回什么值。
功能性代碼的測試將是本文講述的重點,當然我們需要一個測試框架提供支持,本文講述的Jest
就是這種一個框架。
在介紹Jest
之前,還是先聊一聊何謂單元測試,進行有效的單元測試。
說起單元測試,尤其是對于前端行業的同學們來說,真的算的上是讓人聞風喪膽,畢竟單測這個詞聽上去就是牛逼轟轟。
也經常有勇士會說,勞資React、antd一把梭哈,要個雞兒的單元測試。
現實工作中,當你接收了別人的代碼,大多數人可能會覺得在接手一座屎山,得想辦法怎么在屎山上進行雕花,而且還得讓山不崩塌。阿西吧,勞資還不如重構了。
stop!又雙叒叕扯遠了。
好了,別管測試同學嘴里的什么黑盒、白盒、性能、功能。對我們大多數人而言,單元測試,不就是對代碼進行測試么。怎么測試呢?
可能你有點明白了,那么問題來了,挖掘機哪家強?sorry,問題是如何用代碼測試代碼?
舉個簡單的例子,你寫個了函數,判斷請求是否成功。因為不可抗拒的因素,你們公司團隊里面的后端同學,每個人的接口對于成功返回的定義不一樣。有的是通過http 狀態碼200,有的根據返回值中的success是否為true,有的人返回了一個code字段,值可能為200也可能為"200"。
現實就是這么殘酷,前端的小伙伴總是在寫這樣那樣的兼容方案。
// utils.js /** * 判斷請求是否成功,成功返回,失敗提示失敗信息 * @param {Object} response * @returns */ export const requestSuccess = response => { if ( response.data.success === true || response.data.code === 'success' || response.data.code === '1000' || response.data.code === 200 || response.data.code === '200' ) { return response; } message.error(response.data.message || '請求失敗'); };
對于這樣一個常見的函數而言,我們需要如何對其進行單元測試呢?
所謂測試,無非就是在特定場景下(我們可以先不用管這個場景是什么),輸入一些值,得到輸出值,然后跟期望輸出的值進行比對,看是否一致,如果一致則表明這一條測試用例通過。
看這個簡單的例子,我們不用管下面的test
和expect
方法具體是干什么的。
// utils.test.js import { requestSuccess } from './utils.js' test('requestSuccess方法 請求正常測試用例', () => { const input = { data: { success: true, code: 200, message: '請求成功' } }; const expectOutput = input; const output = requestSuccess(input); expect(output).toEqual(expectOutput); });
在上面的案例中,我們來逐步分解:
首先定義了一個輸入:input。
然后將input
作為參數,調用了requestSuccess方法,本次調用得到另一個返回值output。
最后就是判定,判定(也就是所謂的期望/斷言)得到的輸出值output等于期望的輸出值expectOutput。
這是一段最基礎的,正常輸入值的單元測試代碼,我們可以總結出大概的步驟:
1、定義輸入值
2、將輸入值帶上,執行代碼,得到輸出值
3、對輸出值進行斷言
這個斷言就是說你覺得這個輸出值應該是什么,也斷言這個輸出值和你期望輸出值匹配。當然,實際輸出值和你的期望輸出可能不匹配,那就是表明你的這條用例執行失敗。失敗的原因可能是你的源代碼有問題,也可能是你單元測試的用例代碼有問題。
OK,我們了解了最基礎的單元測試。那么真正意義的單元測試應該怎么寫呢?
無非就是寫單元測試用例,定義各種輸入值,判斷和期望輸出是否一致,然后進行分析修改。
再回歸到上面的requestSuccess方法,上面的測試用例僅僅是驗證了正常情況下,當然這種情況可能占大多數,但是單元測試一般就是為了兼容小部分的異常場景。
那么接下來,我們就來分析下一般意義上請求失敗場景的測試用例:
// utils.test.js import { requestSuccess } from './utils'; test('requestSuccess方法 請求失敗測試用例', () => { const input = { data: { success: false, message: '請求失敗' } }; const output = requestSuccess(input); // 沒有返回值,output為undefine expect(output).toBeUndefined(); });
好了,到這里,有的同學說,請求正常、請求異常的情況都覆蓋了,單元測試完成,可以提交測試,然后進行愉快的摸魚了。
等等,事情沒有那么簡單。
測試同學急急忙忙來找你了,說你的程序又崩了,頁面空白了。
你讓測試同學給你復現了,一步一步debug。原來發現,調用你requestSuccess方法的response參數,盡然為一個空對象: {} 。
你可能會直呼好家伙,后端不講武德啊(當然可能性很多,可能并不是后端一個人的鍋),因為不可抗拒因素,你又得去改代碼,一邊改一邊罵。
改完之后的源碼如下,然后你又得意的告訴測試同學已經改完,沒有問題了。
export const requestSuccess = response => { if ( response.data?.success === true || response.data?.code === 'success' || response.data?.code === '1000' || response.data?.code === 200 || response.data?.code === '200' ) { return response; } message.error(response.data.message || '請求失敗'); };
結果不一會,測試同學說,你的程序又崩了,頁面空白了。
你慌了,自言自語的說道,沒問題啊,勞資都寫了兼容了,讓測試同學給你復現了,一步一步debug。原來發現,調用你requestSuccess方法的response參數,盡然為undefined。你破口大罵,告訴測試是后端的鍋,是另一個前端瞎雞兒調用,和你無關。掰扯了一段時間,你又改了下你的代碼:
// 當然下面的代碼還是可以繼續優化 export const requestSuccess = response => { if ( response?.data?.success === true || response?.data?.code === 'success' || response?.data?.code === '1000' || response?.data?.code === 200 || response?.data?.code === '200' ) { return response; } message.error(response.data.message || '請求失敗'); };
再回到單元測試的正題上,上面的那些異常情況,在實際項目運行中比比皆是。而除了配合測試同學發現bug然后修改之外,我們在單元測試的時候即可發現,并優化自己的代碼。
例如requestSuccess針對這個方法而言,我們先不用去管實際調用時候什么請求成功、請求失敗,只去針對這個方法本身,調用requestSuccess方法的參數可能性是非常多的,各種類型的,所以我們可以以每一種類型的輸入值作為一條測試用例。
// utils.test.js import { requestSuccess } from './utils'; // 這個describe可以不用糾結,理解成幾份用例的集合,只是統一為異常輸入的描述 describe('requestSuccess方法異常輸入測試用例', () => { test('response為空對象測試', () => { const input = {}; const output = requestSuccess(input); expect(output).toBeUndefined(); }); test('response為undefined測試', () => { const output = requestSuccess(); expect(output).toBeUndefined(); }); test('response為Number類型測試', () => { const output = requestSuccess(123); expect(output).toBeUndefined(); }); });
在寫了這么多的異常輸入的測試用例之后,你會發現你一開始寫的requestSuccess不夠強大,導致單元測試用例執行失敗,所以你可以一遍又一遍的修改你的源碼,直至測試用例都通過。
總結: 如何進行有效的單元測試,最簡單的做法就是考慮各種異常/邊界輸入值,編寫相應的測試用例,通過單元測試的執行,優化你的代碼。
當然做好單元測試,并不僅僅只是說考慮各種異常輸入即可,實際還會涉及到開發時候 的考慮(比如常說的測試驅動開發之類的)以及非常多的實現細節,這個可能就需要你慢慢的理解了。
官方鏈接
Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!
官方的介紹就是上面2段話,就是說jest是一個讓人愉悅的js測試框架,專注于簡單性。可以配合babel、ts、node、react、angular、vue等其他庫 一起使用。
我們前文提及的什么describe、test、expect方法
等等在Jest
中都有相應的api。
可以使用yarn或者npm進行安裝
yarn add jest -D | npm i jest -D
這里舉了一個簡單的例子,實際組件開發需要使用ts以及其他UI測試框架。
例如開發一個基礎方法,返回2個參數的和。文件名為sum.ts
// sum.js function sum(a, b) { return a + b; } export default sum;
首先我們根據上面的目標文件(sum.js)創建一個測試用例文件-- sum.test.js
, 測試用例文件名稱統一為*.test.js(后綴根據實際場景區分為.js或者.ts或者.tsx)
// sum.test.js import sum from './sum'; test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); });
添加下面的部分到你的package.json中
{ "scripts": { "test": "jest" } }
最后,執行yarn test
ornpm run test
命令,Jest將會打印如下信息:
PASS ./sum.test.js
? adds 1 + 2 to equal 3 (5ms)
就這樣,基于jest的一個基礎單元測試流程走好了,Jest的單元測試核心就是在test
方法的第二個參數里面,expect方法返回一個期望對象,通過匹配器(例如toBe)進行斷言,期望是否和你預期的一致,和預期一致則單元測試通過,不一致則測試無法通過,需要排除問題然后繼續進行單元測試。
更多的配置以及命令行參數請參考官方文檔下面開始講解一些核心API。
在你的測試文件中,Jest將下面這些方法和對象放置全局環境,所以你無需再顯式的去require或者import。當然,如果你更喜歡顯式的import,也可以使用例如import { describe, expect, it } from '@jest/globals'
的方式。
test(name, fn, timeout)
test
有別名it
,兩個方法是一樣的。
第一個參數是你想要描述的測試用例名稱; 第二個參數是包含測試期望的函數,也是測試用例的核心。第三個參數(可選)是超時時間,也就是超過多久將會取消測試(默認是5秒鐘)
Note: 如果fn返回的是個promise,Jest在完成測試前將會等待
Promise
達到resolved
狀態。具體情況本文下面也會講到如何對異步代碼進行測試。
Jest使用匹配器可以讓你使用各種方式測試你的代碼,Jest中的匹配器實際就是expect方法返回的期望
對象中包含的相關方法。官方提供了非常多的匹配器,完善的學習請查看官方文檔。
下面摘選了幾個最常見的匹配器方法。
1、.toBe(value)
toBe是最簡單最基礎的匹配器,就是判定是否精確匹配,toBe
方法使用了Object.is
方法來測試精確相等。
Object.is方法的判定準則可以參考這里
test('two plus two is four', () => { expect(2 + 2).toBe(4); });
在測試中,你有時需要區分 undefined
、 null
,和 false
,但有時你又不需要區分。 Jest 讓你明確你想要什么。
toBeNull
只匹配 null
toBeUndefined
只匹配 undefined
toBeDefined
與 toBeUndefined
相反
toBeTruthy
匹配任何 if
語句為真
toBeFalsy
匹配任何 if
語句為假
2、.not
非常容易理解,一般就是反向測試
test('two plus two is four', () => { expect(2 + 2).not.toBe(4); });
3、.toEqual
遞歸檢查對象或數組的每個字段
和上面的toBe進行對比,toBe對比倆對象對比的是內存地址,toEqual比的是屬性值。
test('object assignment', () => { const data1 = { one: 1, two: 2 }; const data2 = { one: 1, two: 2 }; expect(data1).toBe(data2); // 測試失敗 expect(data1).toEqual(data2);// 測試通過 });
4、expect.assertions
expect.assertions(number)
驗證一定數量的斷言在某個測試用例中被調用。通常在異步代碼測試中非常有用,目的就是為了確保所有的斷言被真實的調用了。
比如下面這個例子,如果去掉了expect.assertions(2)
, 那么測試用例會通過測試,但實際的需求應該是失敗的,因為我們最初的期望是catch中的斷言也會被調用。
而有了expect.assertions(2)
,Jest會判斷斷言實際調用的數量和我們預期是否一致,如果不一致則說明測試失敗。
test('doAsync calls both callbacks', () => { expect.assertions(2); return Promise.resolve(123).then((data: number) => { expect(data).toBe(123); return; // 例如手抖寫了return // 因為某些原因下面的代碼沒有執行 throw new Error('報錯了'); }).catch(err => { expect(err).toBe('報錯了'); }); });
在JavaScript中執行異步代碼是很常見的。 當你有以異步方式運行的代碼時,Jest 需要知道當前它測試的代碼是否已執行完成,然后它可以轉移到另一個測試。 Jest有若干方法處理這種情況。
最常見的異步模式就是回調函數,例如下面的setTimeout方法,下面的測試用例無法通過,原因是Jest無法知道callback具體的調用時間,所以會造成測試已經結束,但是setTimeout的callback還沒有執行。
test('the data is peanut butter', () => { function callback(data: string) { expect(data).toBe('peanut butter'); } setTimeout(() => { callback('peanut butter'); }, 2000); });
想要解決上面的問題,非常簡單,很容易就會聯想到消息通知機制,也就是在callback調用的時候通知Jest,表示當前測試用例通過,可以跑下一個測試。
test方法的第二個參數fn,可以添加一個done參數,done是一個方法,調用了done,就是通知Jest測試完成,當然如果你的測試用例中的done方法始終沒有執行,那么你的測試也會失敗(超時),所以最好的方式就是加上try catch。
test('the data is peanut butter', done => { function callback(data: string) { try { expect(data).toBe('peanut butter'); done(); } catch (err) { done(err); } } setTimeout(() => { callback('peanut butter'); }, 2000); });
如果你的代碼使用了Promise, Jest提供了一種更加直接的方式去處理異步測試。在test第二個參數fn中直接返回一個promise,Jest就會等待這個promise達到resolved
狀態,如果達到了fulfilled
狀態,測試將會自動失敗。
例如這個案例,此測試用例能夠正常的通過
test('promise resolved', () => { return new Promise(resolve => { setTimeout(() => { resolve('resolved'); }, 2000); }).then((data: string) => { expect(data).toBe('resolved'); }); });
如果promise fulfilled
如下,則測試用例會跑失敗
test('promise fulfilled', () => { return Promise.reject('fulfilled').then((data: string) => { expect(data).toBe('fulfilled'); }) });
當然我們也可以使用catch方法,例如下面這個例子,測試用例就能夠正常的通過。
test('promise fulfilled', () => { expect.assertions(1); return Promise.reject('fulfilled').catch(err => { expect(err).toMatch('fulfilled'); }); });
promise
代碼可以配合匹配器.resolves
和rejects
一起使用,使用案例如下:
test('promise resolved', () => { return expect(Promise.resolve('resolved')).resolves.toBe('resolved'); }); test('promise fulfilled', () => { return expect(Promise.reject('fulfilled')).rejects.toMatch('fulfilled'); });
如果你的代碼使用了Promise, Jest提供了一種更加直接的方式去處理異步測試。在test第二個參數fn中直接返回一個promise,Jest就會等待這個promise達到resolved
狀態,如果達到了fulfilled
狀態,測試將會自動失敗。
const TEN = 10; const BASE = 5; function fetchData () { return new Promise((resolve, reject) => { const random = Math.random() * TEN; random > BASE ? resolve(random) : reject(random); }); } test('the random promise', async () => { expect.assertions(1); try { const random = await fetchData(); expect(random).toBeGreaterThan(BASE); } catch (e) { expect(e).toBeLessThanOrEqual(BASE); } });
Mock 函數簡單的說就是模擬一個函數,這個功能很強大,例如nodejs
中沒有DOM/BOM,及時是jsdom
也會缺少一些api,那么我們可以使用mock函數來進行一些測試,具體暫不詳細說明。
有兩種方法可以模擬函數:要么在測試代碼中創建一個 mock 函數,要么編寫一個手動 mock
來覆蓋模塊依賴。
Mock Functions Doc
假設我們要測試函數 forEach
的內部實現,這個函數為傳入的數組中的每個元素調用一次回調函數。
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } }
為了測試此函數,我們可以使用一個 mock 函數,然后檢查 mock 函數的狀態來確保回調函數如期調用。
const mockCallback = jest.fn(x => 42 + x); forEach([0, 1], mockCallback); // 此 mock 函數被調用了兩次 expect(mockCallback.mock.calls.length).toBe(2); // 第一次調用函數時的第一個參數是 0 expect(mockCallback.mock.calls[0][0]).toBe(0); // 第二次調用函數時的第一個參數是 1 expect(mockCallback.mock.calls[1][0]).toBe(1); // 第一次函數調用的返回值是 42 expect(mockCallback.mock.results[0].value).toBe(42);
.mock
屬性所有的 mock 函數都有這個特殊的 .mock
屬性,它保存了關于此函數如何被調用、調用時的返回值的信息。 .mock
屬性還追蹤每次調用時 this
的值,所以我們同樣可以也檢視
const filterTestFn = jest.fn(); // Make the mock return `true` for the first call, // and `false` for the second call filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false); const result = [11, 12].filter(num => filterTestFn(num)); console.log(result); // > [11] console.log(filterTestFn.mock.calls); // > [ [11], [12] ]
在Jest單元測試的真實場景中,會有很多的數據來自接口,但是Jest并不推薦直接在測試代碼中去調用真實的接口,因為這可能會讓測試變得非常緩慢而且脆弱,所以jest.fn().mockResolvedValue提供了mock接口的方式,使用假數據進行測試。
test('async test', async () => { const asyncMock = jest.fn().mockResolvedValue(23); const result = await asyncMock(); // 43 expect(result).toBe(23); });
每當你想要確保你的UI不會有意外的改變,快照測試是非常有用的工具。
一點典型的快照測試案例就是一個移動端的app渲染一個UI組件,拍下快照,然后將其與之前測試存儲的參考快照文件進行對比,如果2個快照不匹配的話測試就會失敗。簡單的來說就是對比前后2次組件渲染的照片,這個測試方法非常適合React這類UI框架。
Jest快照測試第一次會生成一個快照文件,就像下面這樣
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`component--Loading 單元測試start 1`] = ` <div className="container" > <div className="inner" > <div className="boxLoading" /> <div className="title" > 努力加載中... </div> </div> </div> `;
我們可以在Jest 命令中加入--updateSnapshot,這樣快照有跟新的話會跟新快照文件而不是直接讓整個快照測試失敗了。
建議把快照測試文件提交到git,快照測試文件也可以進行code-review。
到此,關于“web前端單元測試之UI測試功能性代碼實例分析”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。