啥?單元測試?我哪有時間寫單元測試?
平常生活中,商品質量永遠是咱們進行選擇時須要着重考慮的因素,計算機軟件也不例外。優秀的軟件應當如咱們預期的同樣工做,可以正確地處理全部功能性需求。優秀的軟件應當如咱們預期同樣,持續穩定運行直到地老天荒。然而,現實生活中的軟件彷佛永遠是那麼脆弱不堪。Bug這個計算機行話隨着廣泛存在的計算機軟件缺陷,逐漸變成了多是被行外人最熟悉的詞彙。因而可知,保障軟件質量實在不是一件容易的事情。javascript
在改進軟件質量這件事情上,人類付出了巨大的努力與探索。在一些最爲關鍵的技術領域,好比分佈式系統的一致性問題中,如 Amazon、Microsoft 等公司採用了形式化驗證的方式檢查軟件系統的正確性。例如,這篇文章介紹了 Amazon 如何利用 TLA+ 檢查並發現 DynamoDB 中若干能夠致使數據丟失的設計 bug。然而在更通常的場景中,咱們並不須要動用形式化驗證這種大殺器,而是採起軟件測試的方式進行。html
大部分人接觸「軟件測試」這個概念的時間遠早於他們的預期。小時候的網絡遊戲,第一次向廣大玩家普及了「內測」、「公測」這樣的概念,雖然可能不少人都並不能意識到這個關乎軟件測試,但這應該是大部分人第一次接觸「軟件測試」這個概念的契機。再日後,更多人是在本科階段的《軟件工程》這門課程中接觸到軟件測試。不管早期的瀑布開發模型亦或是後期的敏捷開發模型再到更現代的極限編程模型,軟件測試都是軟件開發生命週期中不可缺乏的一環,書籍中都會對其進行詳細的介紹。但,到底什麼是軟件測試呢?前端
書本上對軟件測試的正式定義形形色色,但這裏我說說本身的理解。最廣義的說,咱們平常每次「運行」軟件,其實就能當作一次測試;而最狹義的測試裏,咱們會定義軟件的規格,定義軟件的邊界條件,書寫測試用例,編寫自動化測試代碼或者以文檔形式寫出軟件操做步驟,並交於專人驗證開發人員提交的程序是否符合規格定義。可是,通常地說,驗證軟件行爲是否符合需求的行爲就叫作軟件測試。java
因而可知,要理解軟件測試,先要理解軟件需求。react
需求定義了軟件。功能性需求和非功能性需求分別告訴開發者「作什麼」和「作成什麼樣」。好比對於即時通訊軟件,「發送消息」、「接收消息」、「顯示歷史消息」等就屬於功能性需求,他們定義了一個一個的功能點,而「軟件崩潰率小於千分之一」,「可以支持多平臺操做系統」等就屬於非功能性需求。軟件測試就是爲了檢驗軟件是否能知足定義的需求而進行的活動。web
在具備必定規模的軟件開發組織中,必然有專業負責產品質量保證的QA團隊,也即一般意義上的測試團隊。他們對軟件最終出產的質量負責,他們會針對軟件發版時定格的需求,規劃測試用例,進行手動、半自動或全自動測試。還會引入混沌工程,幫助找出一些常規測試手段與使用方法下沒法發現的潛在故障。甚至還會找到目標用戶,邀請他們試用軟件產品,並鼓勵他們幫助團隊尋找軟件中的潛藏缺陷。微軟就曾經以鉅額獎金召集廣大用戶向其提交使用過程當中遇到的缺陷。npm
那麼,既然有如此專業的測試團隊,爲何咱們還須要讓開發人員來寫單元測試呢?編程
對於大中型系統,多人協做與持續改進迭代是常態。一個通過長期迭代的大中型系統中包含了海量特性,這也就使得將來的迭代每每可能牽一髮而動全身。尤爲是當某個新增的功能點須要變動軟件底層設計的時候,咱們所作的修改很容易使得看上去相同的對外接口在一些特定條件下表現出不一樣的行爲。具備單元測試的項目就能夠在修改模塊內部實現後,對模塊對外表現的功能,尤爲是須要知足的特定邊界條件進行測試,從而很容易將隱藏其中的一些問題充分暴露出來。json
全部的自動化軟件測試,最終都要落腳到斷言上。那麼爲了使得被測試的程序能夠被斷言,開發人員不得不事前規劃軟件設計,使得每個單元的關鍵執行結果均可被斷言。所以,當開發人員注意到代碼可測試性後,開發者就會對代碼中每個被測單元的輸入與輸出都很是清晰,依賴也變得清晰,無用的依賴會天然減小,軟件設計變得凝練,可維護性加強。redux
在談了測試的重要性以及單元測試爲什麼須要開發人員編寫以後,咱們來看看什麼是單元。單元測試位於全部測試的最底層,粒度最小,執行速度最快,一般由開發工程師編寫並執行,那麼對於前端開發來講,什麼是「單元」?從目前主流的三大框架的視角看過去,前端的MVVM架構將前端應用分爲了三塊主要部分:View、Model和ViewModel,咱們逐一來看:
從這裏咱們就能發現,前端的單元與後端不一樣,既有以類爲單位的單元,也有以函數爲單位的單元,也有以組件爲單位的單元。而這三類不一樣領域的單元測試又各有特點,讓咱們逐一來看。
在具體討論View、Model與ViewModel層的測試前,先說說單元測試中三個重要的組成部分。咱們能夠把單元測試想象成一場考試,一個一個代碼單元就是這場考試中的考生,測試用例是考試的考題,預期結果是考試的答案。那麼測試三部曲包括了考生、考題與答案,即被測代碼、測試用例與預期結果。
我我的認爲,在測試三部曲中,測試用例佔據了核心地位。測試用例是軟件需求的具體表現形式,它以代碼的形式具體地定義了軟件須要支持的功能,應當作出的反應,須要考慮的邊界條件。被測代碼是測試三部曲必不可少的組成部分,也是咱們工做的核心成果。而預期結果跟隨測試用例,天然就會浮出水面。
A JavaScript library for building user interfaces,React的主頁上如此介紹它本身,事實上也是如此。相較Vue和Angular,React確實專一於更好地進行View層的抽象,不管是提出View = f(props)的思想,仍是單向數據流,都創造性地使得JavaScript構建穩定大型的富交互Web應用成爲可能。咱們以利用React構建的View層爲例說明View層單元測試。
關於View層是否須要編寫單元測試,一直有很大的爭議。
衆所周知,端應用會隨着需求不斷迭代更新,View層測試究竟測試到什麼粒度是一個須要重點權衡的問題。若是測試粒度過細,每每不堪需求變動之擾,而若是測試粒度過粗,與無測試覆蓋也並沒有差異。
業界目前採用的實踐,是對底層組件庫如相似於Antd之類的,徹底與業務無關,組成用戶界面最基本單元的這些空間進行嚴格的單元測試。以Antd爲例,全部的組件位於Antd工程目錄的components文件夾下,每一個組件目錄下的測試都放在__test__文件夾下,在__test__文件夾中,咱們能夠看到全部有關該組件的單元測試,其中值得關注的是__snapshot__這個文件夾,該文件夾中存放了組件在給定條件下的DOM結構,這也意味着Antd的組件渲染測試到DOM這一層級截止。它認爲,DOM結構一致便可知足其對於渲染穩定性的要求。假如瀏覽器的渲染方式或兼容性發生改變,對於同一份DOM渲染結果與以前的版本不一樣,這時Antd組件就有可能出現渲染錯誤,可是Antd的單元測試並不能發現這一點。這體現了Antd對於渲染測試的取捨與判斷。
更細粒度的測試其實也是能夠作的,好比咱們能夠啓動一個瀏覽器,而後將渲染結果截圖保存,以後每次運行測試,一樣截圖並利用相似 Resemble.js 之類的工具進行逐像素比對,這樣測試可以對最終渲染視覺效果負責,可是隨之而來的問題是,一像素的偏差都會致使測試告警,測試在不少狀況下都處於失敗狀態,這無疑也沒有意義。所以如何取捨也是一個見仁見智的問題。
除了渲染測試以外,View層測試還要關注組件內state的變化,一般state的變化會由組件內部事件或者外部事件進行推進的,如點擊事件,表單值改變事件或者網絡請求。
Jest
Jest 是 Facebook 出品的一個測試框架,相對其餘測試框架,其一大特色就是就是內置了經常使用的測試工具,好比自帶斷言、測試覆蓋率工具,實現了開箱即用。而做爲一個面向前端的測試框架, Jest 能夠利用其特有的快照測試功能,經過比對 UI 代碼生成的快照文件,實現對 React 等常見框架的自動測試。此外, Jest 的測試用例是並行執行的,並且只執行發生改變的文件所對應的測試,提高了測試速度。目前在 Github 上其 star 數已經破萬;而除了 Facebook 外,業內其餘公司也開始從其它測試框架轉向 Jest ,好比 Airbnb 的嘗試 ,相信將來 Jest 的發展趨勢仍會比較迅猛。
Jest 能夠經過 npm 或 yarn 進行安裝。以 npm 爲例,既可用 npm install -g jest 進行全局安裝;也能夠只局部安裝、並在 package.json 中指定 test 腳本:
{ "scripts": { "test": "jest" } }
Jest的基本使用
表示測試用例是一個測試框架提供的最基本的 API , Jest 內部使用了 Jasmine 2 來進行測試,故其用例語法與 Jasmine 相同。test()函數來描述一個測試用例,舉個簡單的例子:
// 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 請參考文檔)。寫完了用例,運行在項目目錄下執行npm test,便可看到測試結果:
若測試失敗,會標識出失敗的斷言位置,結果以下:
Jest中,你還能夠對每一個測試前須要作的工做和測試後須要作的工做進行統一處理,對測試文件中全部的用例進行統一的預處理,可使用 beforeAll() 函數;而若是想在每一個用例開始前進行都預處理,則可以使用 beforeEach() 函數。至於後處理,也有對應的 afterAll() 和 afterEach() 函數。
若是隻是想對某幾個用例進行一樣的預處理或後處理,能夠將先將這幾個用例歸爲一組。使用 describe() 函數便可表示一組用例,再將上面提到的四個處理函數置於 describe() 的處理回調內,就實現了對一組用例的預處理或後處理:
describe('test testObject', () => { beforeAll(() => { // 預處理操做 }) test('is foo', () => { expect(testObject.foo).toBeTruthy() }) test('is not bar', () => { expect(testObject.bar).toBeFalsy() }) afterAll(() => { // 後處理操做 }) })
咱們還可使用 jest 測試異步代碼。異步代碼的測試關鍵在於告知測試框架,待測的異步代碼如何完成。Jest提供了兩種常見的異步代碼調用方式的測試方法。
回調函數:
// 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會給測試函數注入done函數,你只須要在回調函數執行末尾調用done函數,便可告訴jest,改異步調用已經完成。
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); // 確保至少有一個斷言被調用,不然測試失敗 return promiseHello('world').then((data) => { expect(data).toBe('Hello world') }) })
對於Promise形式的異步執行方式,能夠直接在promise以後的then中進行斷言。
另外,jest還支持async/await的異步執行方式,與同步同樣,只須要在await後直接斷言便可。
Jest還經過集成Istanbul支持了測試覆蓋率統計。能夠經過增長命令行參數 --coverage 實現,也可在 package.json 文件中進行更詳細的配置。
// 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') // }); })
運行 jest --coverage 可看到在根目錄下生成一個測試覆蓋率報告目錄 coverage ,打開其中的 index.html :
該網頁展現了代碼覆蓋率和未測試的行數,具體統計方式能夠查看Istanbul的詳細說明。
若是咱們去掉 branches.test.js 中的註釋,測試覆蓋率則變成100%:
react-test-renderer
Jest提供了快照測試功能,而 react-test-renderer 能夠根據 React 的 Virtual DOM 結構生成一個符合 Jest 規範的快照,如此,即可以對渲染結果進行基於 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(); });
咱們先構造上述測試,運行後獲得下述快照文件:
exports[`renders correctly 1`] = ` <a className="normal" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]} > Facebook </a> `;
這個可讀的快照文件以可讀的形式展現了 React 渲染出的 DOM 結構。相比於肉眼觀察效果的 UI 測試,快照測試直接由Jest進行比對、速度更快;並且因爲直接展現了 DOM 結構,也能讓咱們在檢查快照的時候,快速、準確地發現問題。
Enzyme & React Testing Library
Jest 提供了單元測試最基本的一些功能:獨立的測試環境,統一的setup、teardown,斷言庫,異步測試功能,函數 mock 、stub 和 spy,測試覆蓋率統計等,可是咱們的View層測試仍是須要將 React 組件進行渲染,並在渲染的組件上進行一些操做的。React 官方提供了 Test Utility,而 Enzyme 和 React Testing Library 則是在官方的 Test Utility 基礎之上進行了封裝,使得測試更加方便。
Enzyme 介紹的文章較 React Testing Library 更多,可是在 React 16 出現後,尤爲是 React Hooks 出現後,Enzyme採起的打補丁的適配方式有一些根本問題沒法解決。React Testing Library在FAQ中談了關於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的做者認爲測試應當模仿用戶使用產品時進行的操做,而非對實現細節進行測試。即應當進行黑盒測試而非白盒。
官網 https://testing-library.com/docs/recipes 的 recipe 是單元測試很是不錯的指南,值得一看。
前端的Model層有一個更加廣爲人知的名字:狀態管理。細提及來又是流派之爭:以單向數據流和函數式思想爲基石的Redux,一樣受單向數據流影響,但又受到響應式編程啓發的MobX,FPR流派表明的Rxjs。而後因爲Redux並不支持異步操做,因而又孕育而生許多異步中間件以加強Redux的功能好比redux-thunk,redux-saga,redux-observer等等。斯坦福大學iOS開發課上,教授介紹MVC時說了這麼一句話:Model定義了軟件應用。那麼因而可知,不一樣的應用下,不一樣的Model層解決方案應該各有其優點與劣勢。
咱們以Redux爲例說明Model層測試的關注點。在Redux中咱們一般會分開測試reducer和actionCreator。Reducer必是純函數,因此其測試相對容易,一般也不會有什麼外部依賴出現,即便存在,因爲其純函數特性,也很容易進行mock。而actionCreator相對複雜,因爲使用不一樣的中間件,actionCreator的形式差異會很大,可是歸根究底,就是要測試其在各個流程中是否能如預期的那樣完成各個異步操做,並建立出符合預期的,最終交付給reducer的action對象。
Model層測試常見的工具除了View層也須要用到的Jest測試框架外,還須要根據工程中的Model層庫選型選擇相對應的工具,這些工具的主要目的是爲了提供一些測試輔助的手段,好比Redux的測試須要創造一個假的store來使整個程序可以運行起來。RxJS則須要模擬其複雜的異步事件流,即「彈珠測試」。
我所理解的ViewModel層,更多的是一些數據接口,數據格式轉換,數據校驗等等之類的工具性質的函數。因爲後端接口給咱們的數據與真正頁面上展現的形式一般有較大的差別,咱們須要在真正將數據給到View層以前,通過ViewModel層進行轉化。一樣的,因爲用戶在頁面上進行的輸入信息有時也會不符合軟件定義,錯誤的用戶輸入會形成系統安全隱患,所以也須要在ViewModel層對用戶輸入的數據進行校驗。
因爲ViewModel層的純函數性質,一般只須要Jest庫便可進行,過程通常比較簡單,在此就不贅述。