前端如何实现网络速度测试功能_前端组件单元测试
從軟件質(zhì)量說起
日常生活中,商品質(zhì)量永遠(yuǎn)是我們進(jìn)行選擇時(shí)需要著重考慮的因素,計(jì)算機(jī)軟件也不例外。優(yōu)秀的軟件應(yīng)當(dāng)如我們預(yù)期的一樣工作,能夠正確地處理所有功能性需求。優(yōu)秀的軟件應(yīng)當(dāng)如我們預(yù)期一樣,持續(xù)穩(wěn)定運(yùn)行直到地老天荒。然而,現(xiàn)實(shí)生活中的軟件似乎永遠(yuǎn)是那么脆弱不堪。Bug這個(gè)計(jì)算機(jī)行話隨著普遍存在的計(jì)算機(jī)軟件缺陷,逐漸變成了可能是被行外人最熟悉的詞匯。由此可見,保障軟件質(zhì)量實(shí)在不是一件容易的事情。
在改進(jìn)軟件質(zhì)量這件事情上,人類付出了巨大的努力與探索。在一些最為關(guān)鍵的技術(shù)領(lǐng)域,比如分布式系統(tǒng)的一致性問題中,如 Amazon、Microsoft 等公司采用了形式化驗(yàn)證的方式檢查軟件系統(tǒng)的正確性。例如,這篇文章介紹了 Amazon 如何利用 TLA+ 檢查并發(fā)現(xiàn) DynamoDB 中若干可以導(dǎo)致數(shù)據(jù)丟失的設(shè)計(jì) bug。然而在更一般的場景中,我們并不需要?jiǎng)佑眯问交?yàn)證這種大殺器,而是采取軟件測試的方式進(jìn)行。
到底軟件測試是啥?
大部分人接觸“軟件測試”這個(gè)概念的時(shí)間遠(yuǎn)早于他們的預(yù)期。小時(shí)候的網(wǎng)絡(luò)游戲,第一次向廣大玩家普及了“內(nèi)測”、“公測”這樣的概念,雖然可能很多人都并不能意識到這個(gè)關(guān)乎軟件測試,但這應(yīng)該是大部分人第一次接觸“軟件測試”這個(gè)概念的契機(jī)。再往后,更多人是在本科階段的《軟件工程》這門課程中接觸到軟件測試。無論早期的瀑布開發(fā)模型亦或是后期的敏捷開發(fā)模型再到更現(xiàn)代的極限編程模型,軟件測試都是軟件開發(fā)生命周期中不可缺少的一環(huán),書籍中都會(huì)對其進(jìn)行詳細(xì)的介紹。但,到底什么是軟件測試呢?
書本上對軟件測試的正式定義形形色色,但這里我說說自己的理解。最廣義的說,我們?nèi)粘C看巍斑\(yùn)行”軟件,其實(shí)就能看成一次測試;而最狹義的測試?yán)?#xff0c;我們會(huì)定義軟件的規(guī)格,定義軟件的邊界條件,書寫測試用例,編寫自動(dòng)化測試代碼或者以文檔形式寫出軟件操作步驟,并交于專人驗(yàn)證開發(fā)人員提交的程序是否符合規(guī)格定義。但是,一般地說,驗(yàn)證軟件行為是否符合需求的行為就叫做軟件測試。
由此可見,要理解軟件測試,先要理解軟件需求。
功能性需求與非功能性需求
需求定義了軟件。功能性需求和非功能性需求分別告訴開發(fā)者「做什么」和「做成什么樣」。比如對于即時(shí)通信軟件,「發(fā)送消息」、「接收消息」、「顯示歷史消息」等就屬于功能性需求,他們定義了一個(gè)一個(gè)的功能點(diǎn),而「軟件崩潰率小于千分之一」,「能夠支持多平臺操作系統(tǒng)」等就屬于非功能性需求。軟件測試就是為了檢驗(yàn)軟件是否能滿足定義的需求而進(jìn)行的活動(dòng)。
為什么單元測試需要開發(fā)人員來寫?
在具有一定規(guī)模的軟件開發(fā)組織中,必然有專業(yè)負(fù)責(zé)產(chǎn)品質(zhì)量保證的QA團(tuán)隊(duì),也即通常意義上的測試團(tuán)隊(duì)。他們對軟件最終出產(chǎn)的質(zhì)量負(fù)責(zé),他們會(huì)針對軟件發(fā)版時(shí)定格的需求,規(guī)劃測試用例,進(jìn)行手動(dòng)、半自動(dòng)或全自動(dòng)測試。還會(huì)引入混沌工程,幫助找出一些常規(guī)測試手段與使用方法下無法發(fā)現(xiàn)的潛在故障。甚至還會(huì)找到目標(biāo)用戶,邀請他們試用軟件產(chǎn)品,并鼓勵(lì)他們幫助團(tuán)隊(duì)尋找軟件中的潛藏缺陷。微軟就曾經(jīng)以巨額獎(jiǎng)金召集廣大用戶向其提交使用過程中遇到的缺陷。
那么,既然有如此專業(yè)的測試團(tuán)隊(duì),為什么我們還需要讓開發(fā)人員來寫單元測試呢?
單元測試能夠幫助大中型系統(tǒng)快速迭代
對于大中型系統(tǒng),多人協(xié)作與持續(xù)改進(jìn)迭代是常態(tài)。一個(gè)經(jīng)過長期迭代的大中型系統(tǒng)中包含了海量特性,這也就使得未來的迭代往往可能牽一發(fā)而動(dòng)全身。尤其是當(dāng)某個(gè)新增的功能點(diǎn)需要變更軟件底層設(shè)計(jì)的時(shí)候,我們所做的修改很容易使得看上去相同的對外接口在一些特定條件下表現(xiàn)出不同的行為。具有單元測試的項(xiàng)目就可以在修改模塊內(nèi)部實(shí)現(xiàn)后,對模塊對外表現(xiàn)的功能,尤其是需要滿足的特定邊界條件進(jìn)行測試,從而很容易將隱藏其中的一些問題充分暴露出來。
單元測試能夠幫助開發(fā)人員改進(jìn)軟件設(shè)計(jì)
所有的自動(dòng)化軟件測試,最終都要落腳到斷言上。那么為了使得被測試的程序可以被斷言,開發(fā)人員不得不事前規(guī)劃軟件設(shè)計(jì),使得每一個(gè)單元的關(guān)鍵執(zhí)行結(jié)果都可被斷言。因此,當(dāng)開發(fā)人員注意到代碼可測試性后,開發(fā)者就會(huì)對代碼中每一個(gè)被測單元的輸入與輸出都非常清晰,依賴也變得清晰,無用的依賴會(huì)自然減少,軟件設(shè)計(jì)變得凝練,可維護(hù)性增強(qiáng)。
什么是前端的單元
在談了測試的重要性以及單元測試為何需要開發(fā)人員編寫之后,我們來看看什么是單元。單元測試位于所有測試的最底層,粒度最小,執(zhí)行速度最快,通常由開發(fā)工程師編寫并執(zhí)行,那么對于前端開發(fā)來說,什么是「單元」?從目前主流的三大框架的視角看過去,前端的MVVM架構(gòu)將前端應(yīng)用分為了三塊主要部分:View、Model和ViewModel,我們逐一來看:
- View層,JSX或者template,通常的表現(xiàn)形式為無狀態(tài)組件或者純函數(shù)式組件,給定Props的情況下一定會(huì)有相同的DOM或者VDOM被渲染出來。
- Model層,狀態(tài)管理工具所處的位置,通常會(huì)利用Vuex、Redux、MobX、RxJS等工具進(jìn)行編寫,視圖無關(guān),通常從ViewModel中得到輸入,執(zhí)行一些副作用,或者將輸出更新到ViewModel上。
- ViewModel層,通常與View層雙向綁定,但是當(dāng)View與其不能完美適配時(shí),ViewModel層負(fù)責(zé)將數(shù)據(jù)依據(jù)View的需求進(jìn)行轉(zhuǎn)化,因此該層是大量工具函數(shù)的應(yīng)用之處。如各種各樣的Formatter、各種各樣的Filter等往往位于這一層。
從這里我們就能發(fā)現(xiàn),前端的單元與后端不同,既有以類為單位的單元,也有以函數(shù)為單位的單元,也有以組件為單位的單元。而這三類不同領(lǐng)域的單元測試又各有特色,讓我們逐一來看。
測試三部曲
在具體討論View、Model與ViewModel層的測試前,先說說單元測試中三個(gè)重要的組成部分。我們可以把單元測試想象成一場考試,一個(gè)一個(gè)代碼單元就是這場考試中的考生,測試用例是考試的考題,預(yù)期結(jié)果是考試的答案。那么測試三部曲包括了考生、考題與答案,即被測代碼、測試用例與預(yù)期結(jié)果。
我個(gè)人認(rèn)為,在測試三部曲中,測試用例占據(jù)了核心地位。測試用例是軟件需求的具體表現(xiàn)形式,它以代碼的形式具體地定義了軟件需要支持的功能,應(yīng)當(dāng)做出的反應(yīng),需要考慮的邊界條件。被測代碼是測試三部曲必不可少的組成部分,也是我們工作的核心成果。而預(yù)期結(jié)果跟隨測試用例,自然就會(huì)浮出水面。
View層單元測試
A JavaScript library for building user interfaces,React的主頁上如此介紹它自己,事實(shí)上也是如此。相較Vue和Angular,React確實(shí)專注于更好地進(jìn)行View層的抽象,無論是提出View = f(props)的思想,還是單向數(shù)據(jù)流,都創(chuàng)造性地使得JavaScript構(gòu)建穩(wěn)定大型的富交互Web應(yīng)用成為可能。我們以利用React構(gòu)建的View層為例說明View層單元測試。
View層單元測試的關(guān)注點(diǎn)
關(guān)于View層是否需要編寫單元測試,一直有很大的爭議。
眾所周知,端應(yīng)用會(huì)隨著需求不斷迭代更新,View層測試究竟測試到什么粒度是一個(gè)需要重點(diǎn)權(quán)衡的問題。如果測試粒度過細(xì),往往不堪需求變更之?dāng)_,而如果測試粒度過粗,與無測試覆蓋也并無差別。
業(yè)界目前采用的實(shí)踐,是對底層組件庫如類似于Antd之類的,完全與業(yè)務(wù)無關(guān),組成用戶界面最基本單元的這些空間進(jìn)行嚴(yán)格的單元測試。以Antd為例,所有的組件位于Antd工程目錄的components文件夾下,每個(gè)組件目錄下的測試都放在__test__文件夾下,在__test__文件夾中,我們可以看到所有有關(guān)該組件的單元測試,其中值得關(guān)注的是__snapshot__這個(gè)文件夾,該文件夾中存放了組件在給定條件下的DOM結(jié)構(gòu),這也意味著Antd的組件渲染測試到DOM這一層級截止。它認(rèn)為,DOM結(jié)構(gòu)一致即可滿足其對于渲染穩(wěn)定性的要求。假如瀏覽器的渲染方式或兼容性發(fā)生改變,對于同一份DOM渲染結(jié)果與之前的版本不同,這時(shí)Antd組件就有可能出現(xiàn)渲染錯(cuò)誤,但是Antd的單元測試并不能發(fā)現(xiàn)這一點(diǎn)。這體現(xiàn)了Antd對于渲染測試的取舍與判斷。
更細(xì)粒度的測試其實(shí)也是可以做的,比如我們可以啟動(dòng)一個(gè)瀏覽器,然后將渲染結(jié)果截圖保存,之后每次運(yùn)行測試,同樣截圖并利用類似 Resemble.js 之類的工具進(jìn)行逐像素比對,這樣測試能夠?qū)ψ罱K渲染視覺效果負(fù)責(zé),但是隨之而來的問題是,一像素的誤差都會(huì)導(dǎo)致測試告警,測試在很多情況下都處于失敗狀態(tài),這無疑也沒有意義。因此如何取舍也是一個(gè)見仁見智的問題。
除了渲染測試之外,View層測試還要關(guān)注組件內(nèi)state的變化,通常state的變化會(huì)由組件內(nèi)部事件或者外部事件進(jìn)行推動(dòng)的,如點(diǎn)擊事件,表單值改變事件或者網(wǎng)絡(luò)請求。
基于React的View層測試需要用到的工具
Jest
Jest 是 Facebook 出品的一個(gè)測試框架,相對其他測試框架,其一大特點(diǎn)就是就是內(nèi)置了常用的測試工具,比如自帶斷言、測試覆蓋率工具,實(shí)現(xiàn)了開箱即用。而作為一個(gè)面向前端的測試框架, Jest 可以利用其特有的快照測試功能,通過比對 UI 代碼生成的快照文件,實(shí)現(xiàn)對 React 等常見框架的自動(dòng)測試。此外, Jest 的測試用例是并行執(zhí)行的,而且只執(zhí)行發(fā)生改變的文件所對應(yīng)的測試,提升了測試速度。目前在 Github 上其 star 數(shù)已經(jīng)破萬;而除了 Facebook 外,業(yè)內(nèi)其他公司也開始從其它測試框架轉(zhuǎn)向 Jest ,比如 Airbnb 的嘗試 ,相信未來 Jest 的發(fā)展趨勢仍會(huì)比較迅猛。
Jest 可以通過 npm 或 yarn 進(jìn)行安裝。以 npm 為例,既可用 npm install -g jest 進(jìn)行全局安裝;也可以只局部安裝、并在 package.json 中指定 test 腳本:
{"scripts": {"test": "jest"} }Jest的基本使用
表示測試用例是一個(gè)測試框架提供的最基本的 API , Jest 內(nèi)部使用了 Jasmine 2 來進(jìn)行測試,故其用例語法與 Jasmine 相同。test()函數(shù)來描述一個(gè)測試用例,舉個(gè)簡單的例子:
// hello.js module.exports = () => 'Hello world' // hello.test.js let hello = require('hello.js')test('should get "Hello world"', () => {expect(hello()).toBe('Hello world') // 測試成功// expect(hello()).toBe('Hello') // 測試失敗 })其中toBe('Hello world')便是一句斷言( Jest 管它叫 “matcher” ,想了解更多 matcher 請參考文檔)。寫完了用例,運(yùn)行在項(xiàng)目目錄下執(zhí)行npm test,即可看到測試結(jié)果:
若測試失敗,會(huì)標(biāo)識出失敗的斷言位置,結(jié)果如下:
Jest中,你還可以對每個(gè)測試前需要做的工作和測試后需要做的工作進(jìn)行統(tǒng)一處理,對測試文件中所有的用例進(jìn)行統(tǒng)一的預(yù)處理,可以使用 beforeAll() 函數(shù);而如果想在每個(gè)用例開始前進(jìn)行都預(yù)處理,則可使用 beforeEach() 函數(shù)。至于后處理,也有對應(yīng)的 afterAll() 和 afterEach() 函數(shù)。
如果只是想對某幾個(gè)用例進(jìn)行同樣的預(yù)處理或后處理,可以將先將這幾個(gè)用例歸為一組。使用 describe() 函數(shù)即可表示一組用例,再將上面提到的四個(gè)處理函數(shù)置于 describe() 的處理回調(diào)內(nèi),就實(shí)現(xiàn)了對一組用例的預(yù)處理或后處理:
describe('test testObject', () => {beforeAll(() => {// 預(yù)處理操作})test('is foo', () => {expect(testObject.foo).toBeTruthy()})test('is not bar', () => {expect(testObject.bar).toBeFalsy()})afterAll(() => {// 后處理操作}) })我們還可以使用 jest 測試異步代碼。異步代碼的測試關(guān)鍵在于告知測試框架,待測的異步代碼如何完成。Jest提供了兩種常見的異步代碼調(diào)用方式的測試方法。
回調(diào)函數(shù):
// asyncHello.js module.exports = (name, cb) => setTimeout(() => cb(`Hello ${name}`), 1000) // asyncHello.test.js let asyncHello = require('asyncHello.js')test('should get "Hello world"', (done) => {asyncHello('world', (result) => {expect(result).toBe('Hello world')done()}) })jest會(huì)給測試函數(shù)注入done函數(shù),你只需要在回調(diào)函數(shù)執(zhí)行末尾調(diào)用done函數(shù),即可告訴jest,改異步調(diào)用已經(jīng)完成。
Promise:
// promiseHello.js module.exports = (name) => {return new Promise((resolve) => {setTimeout(() => resolve(`Hello ${name}`), 1000)}) } // promiseHello.test.js let promiseHello = require('promiseHello.js')it('should get "Hello world"', () => {expect.assertions(1); // 確保至少有一個(gè)斷言被調(diào)用,否則測試失敗return promiseHello('world').then((data) => {expect(data).toBe('Hello world')}) })對于Promise形式的異步執(zhí)行方式,可以直接在promise之后的then中進(jìn)行斷言。
另外,jest還支持async/await的異步執(zhí)行方式,與同步一樣,只需要在await后直接斷言即可。
Jest還通過集成Istanbul支持了測試覆蓋率統(tǒng)計(jì)。可以通過增加命令行參數(shù) --coverage 實(shí)現(xiàn),也可在 package.json 文件中進(jìn)行更詳細(xì)的配置。
// branches.js module.exports = (name) => {if (name === 'Levon') {return `Hello Levon`} else {return `Hello ${name}`} } // branches.test.js let branches = require('../branches.js')describe('Multiple branches test', ()=> {test('should get Hello Levon', ()=> {expect(branches('Levon')).toBe('Hello Levon')});// test('should get Hello World', ()=> {// expect(branches('World')).toBe('Hello World')// }); })運(yùn)行 jest --coverage 可看到在根目錄下生成一個(gè)測試覆蓋率報(bào)告目錄 coverage ,打開其中的 index.html :
該網(wǎng)頁展示了代碼覆蓋率和未測試的行數(shù),具體統(tǒng)計(jì)方式可以查看Istanbul的詳細(xì)說明。
如果我們?nèi)サ?branches.test.js 中的注釋,測試覆蓋率則變成100%:
react-test-renderer
Jest提供了快照測試功能,而 react-test-renderer 可以根據(jù) React 的 Virtual DOM 結(jié)構(gòu)生成一個(gè)符合 Jest 規(guī)范的快照,如此,便可以對渲染結(jié)果進(jìn)行基于 DOM 的比對:
import React from 'react'; import Link from '../Link.react'; import renderer from 'react-test-renderer';it('renders correctly', () => {const tree = renderer.create(<Link page="http://www.facebook.com">Facebook</Link>).toJSON();expect(tree).toMatchSnapshot(); });我們先構(gòu)造上述測試,運(yùn)行后得到下述快照文件:
exports[`renders correctly 1`] = ` <aclassName="normal"href="http://www.facebook.com"onMouseEnter={[Function]}onMouseLeave={[Function]} >Facebook </a> `;exports[`renders correctly 1`] = ` <aclassName="normal"href="http://www.facebook.com"onMouseEnter={[Function]}onMouseLeave={[Function]} >Facebook </a> `;這個(gè)可讀的快照文件以可讀的形式展示了 React 渲染出的 DOM 結(jié)構(gòu)。相比于肉眼觀察效果的 UI 測試,快照測試直接由Jest進(jìn)行比對、速度更快;而且由于直接展示了 DOM 結(jié)構(gòu),也能讓我們在檢查快照的時(shí)候,快速、準(zhǔn)確地發(fā)現(xiàn)問題。
Enzyme & React Testing Library
Jest 提供了單元測試最基本的一些功能:獨(dú)立的測試環(huán)境,統(tǒng)一的setup、teardown,斷言庫,異步測試功能,函數(shù) mock 、stub 和 spy,測試覆蓋率統(tǒng)計(jì)等,但是我們的View層測試還是需要將 React 組件進(jìn)行渲染,并在渲染的組件上進(jìn)行一些操作的。React 官方提供了 Test Utility,而 Enzyme 和 React Testing Library 則是在官方的 Test Utility 基礎(chǔ)之上進(jìn)行了封裝,使得測試更加方便。
Enzyme 介紹的文章較 React Testing Library 更多,但是在 React 16 出現(xiàn)后,尤其是 React Hooks 出現(xiàn)后,Enzyme采取的打補(bǔ)丁的適配方式有一些根本問題無法解決。React Testing Library在FAQ中談了關(guān)于Enzyme的一些看法:
What about enzyme is "bloated with complexity and features" and "encourage poor testing practices"?
Most of the damaging features have to do with encouraging testing implementation details. Primarily, these are shallow rendering, APIs which allow selecting rendered elements by component constructors, and APIs which allow you to get and interact with component instances (and their state/properties) (most of enzyme's wrapper APIs allow this).
The guiding principle for this library is:
The more your tests resemble the way your software is used, the more confidence they can give you. - 17 Feb 2018
React Testing Library的作者認(rèn)為測試應(yīng)當(dāng)模仿用戶使用產(chǎn)品時(shí)進(jìn)行的操作,而非對實(shí)現(xiàn)細(xì)節(jié)進(jìn)行測試。即應(yīng)當(dāng)進(jìn)行黑盒測試而非白盒。
官網(wǎng) https://testing-library.com/docs/recipes 的 recipe 是單元測試非常不錯(cuò)的指南,值得一看。這個(gè)可讀的快照文件以可讀的形式展示了 React 渲染出的 DOM 結(jié)構(gòu)。相比于肉眼觀察效果的 UI 測試,快照測試直接由Jest進(jìn)行比對、速度更快;而且由于直接展示了 DOM 結(jié)構(gòu),也能讓我們在檢查快照的時(shí)候,快速、準(zhǔn)確地發(fā)現(xiàn)問題。
Model層單元測試
前端的Model層有一個(gè)更加廣為人知的名字:狀態(tài)管理。細(xì)說起來又是流派之爭:以單向數(shù)據(jù)流和函數(shù)式思想為基石的Redux,同樣受單向數(shù)據(jù)流影響,但又受到響應(yīng)式編程啟發(fā)的MobX,FPR流派代表的Rxjs。然后由于Redux并不支持異步操作,于是又孕育而生許多異步中間件以增強(qiáng)Redux的功能比如redux-thunk,redux-saga,redux-observer等等。斯坦福大學(xué)iOS開發(fā)課上,教授介紹MVC時(shí)說了這么一句話:Model定義了軟件應(yīng)用。那么由此可見,不同的應(yīng)用下,不同的Model層解決方案應(yīng)該各有其優(yōu)勢與劣勢。
我們以Redux為例說明Model層測試的關(guān)注點(diǎn)。在Redux中我們通常會(huì)分開測試reducer和actionCreator。Reducer必是純函數(shù),所以其測試相對容易,通常也不會(huì)有什么外部依賴出現(xiàn),即使存在,由于其純函數(shù)特性,也很容易進(jìn)行mock。而actionCreator相對復(fù)雜,由于使用不同的中間件,actionCreator的形式差別會(huì)很大,但是歸根究底,就是要測試其在各個(gè)流程中是否能如預(yù)期的那樣完成各個(gè)異步操作,并創(chuàng)建出符合預(yù)期的,最終交付給reducer的action對象。
Model層測試常見的工具除了View層也需要用到的Jest測試框架外,還需要根據(jù)工程中的Model層庫選型選擇相對應(yīng)的工具,這些工具的主要目的是為了提供一些測試輔助的手段,比如Redux的測試需要?jiǎng)?chuàng)造一個(gè)假的store來使整個(gè)程序能夠運(yùn)行起來。RxJS則需要模擬其復(fù)雜的異步事件流,即「彈珠測試」。
ViewModel層單元測試
我所理解的ViewModel層,更多的是一些數(shù)據(jù)接口,數(shù)據(jù)格式轉(zhuǎn)換,數(shù)據(jù)校驗(yàn)等等之類的工具性質(zhì)的函數(shù)。由于后端接口給我們的數(shù)據(jù)與真正頁面上展示的形式通常有較大的差異,我們需要在真正將數(shù)據(jù)給到View層之前,經(jīng)過ViewModel層進(jìn)行轉(zhuǎn)化。同樣的,由于用戶在頁面上進(jìn)行的輸入信息有時(shí)也會(huì)不符合軟件定義,錯(cuò)誤的用戶輸入會(huì)造成系統(tǒng)安全隱患,因此也需要在ViewModel層對用戶輸入的數(shù)據(jù)進(jìn)行校驗(yàn)。
由于ViewModel層的純函數(shù)性質(zhì),通常只需要Jest庫即可進(jìn)行,過程一般比較簡單,在此就不贅述。
總結(jié)
以上是生活随笔為你收集整理的前端如何实现网络速度测试功能_前端组件单元测试的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql配置管理器服务是空的_Postgr
- 下一篇: 三维坐标系带偏航角俯仰角_浅谈三维旋转的