最近在作 Coding 企業版 前端開發時花了不少時間寫測試,因而和你們分享一些前端開發中的測試概念與方法。css
我理解的寫測試實際上是你寫一些代碼來驗證你所謂的能夠交付的代碼是你所預期的設計,有一些朋友叫他 TDD 也就是測試驅動型的設計,其實究竟是先寫代碼仍是先寫測試,並非最重要的,卻是能給你信心這個代碼是符合設計的更重要。html
這個問題不是這篇分享要和你們聊的,可是做爲曾經也有這樣疑問的我仍是簡單提一下。咱們常常過於自信本身的代碼,由於編寫的時候已經作過 debug 調試,完過後以爲足夠了,或者期待下次重構再調整之。結果遇到 bug 沒法最快時間肯定問題,別人接手代碼也不知道這個模塊的設計意圖和使用方法,必須跳進去讀代碼,也不清楚改了一些內容後會不會影響這個模塊功能,又得耗時再次 debug 。在弱類型的語言尤爲前端開發中尤其明顯。那種決定暫時棄之而不顧的的思想很可怕,由於咱們沒有聽過過勒布朗法則:稍後等於永不。前端
從字面意思理解, 寫一段代碼來測試一個單元。何爲單元?其實和編程語言相關,他有多是一個 function,一個 module 一個 package 一個類,固然在 JavaScript 中也頗有可能只是一個 object 。既然如此,那麼測試這樣的一個小塊基本上就是比較孤立,單獨驗證這個小塊的邏輯,一個 function 的輸入輸出,一個算法的功能和複雜度等等。接下來舉幾個企業版前端開發中的實際案例。react
咱們使用 jest 做爲測試框架(斷言庫)。jest 會自動搜索全部文件目錄下的. spec.js 結尾的文件,而後執行測試。斷言庫其實還有不少,他們都具有相似 describe , it , expect 些 api。對於一個沒有其餘依賴的純函數,例如 redux 中同步 action 或 reducer。 咱們要測的固然就是輸入用例而後對應輸出是否符合預期ios
it('should return showMore action', () => {
expect(showMore()).toEqual({ type: ACTION.DEMO_LIST_REMOVE_ITEM, }); });
咱們注意到這樣的一個 function 並無 I/O 和 UI 上的依賴,他更有利於作單元測試。其中的 it 接受一個 string 參數,描述一個小測試。另外一個就是測試方法體函數,it 這種測試不能單獨使用,通常都包在一個 describe 方法下成爲的方法組。那方法體裏寫什麼呢,其實我也能夠寫成web
if (showMore().type !== ACTION.DEMO_LIST_REMOVE_ITEM) throw 'failed'
只要拋出異常那麼框架就會認爲這條測試跑不過。固然 expect 則 api 更加的漂亮,擁有 toEqual toBe、toMapSnap
shot 等判斷 api 肯定兩個條件之間的關係.
對於純函數的測試並不難,難的仍是如何把代碼寫的更可單元測試化,而不要有太多的依賴。算法
事實上不少狀況小塊代碼仍是會有函數和 I/O 依賴,好比一些 code 依賴 Ajax 或者 localStorage 或者 IndexedDB ,這樣的代碼是不能被 united-test 的,因而咱們須要 mock 相應依賴的接口拿到上下文測試咱們的代碼,這樣的測試叫集成測試。咱們項目中主要依賴了 js-dom 和異步的 action 。下面分別討論編程
事實上不少狀況函數仍是會有函數和 I/O 依賴,最典型的就是異步 action 等,他的 I/O 可能會依賴 store.getState(), 自身又會依賴異步中間鍵。這類使用原生 js 測試起來是比較困難的。咱們思考咱們測試目的,即當咱們觸發了一個 action 後它經歷了一個圈異步最終 store.getAction 中這個 action 拿到的數據是否和咱們預期一致。既然你們依賴 redux 中 store 的生命週期與 store,因而咱們須要兩個工具庫 redux-mock-store 和 nock ,因而測試就變成了這樣。redux
import configureMockStore from 'redux-mock-store';
import nock from 'nock';
import thunk from 'redux-thunk';
const mockStore = configureMockStore([thunk]);
// 配置mock的store,也讓他有咱們相同的middleware
describe('get billings actions', () => {
afterEach(() => nock.cleanAll());// 每執行完一個測試後清空nock it('create get all Billings action', () => { const store = mockStore({ // 以咱們約定的初始state建立store,控制I/O依賴 APP: { enterprise: { key : 'codingcorp' } } }); const data = [ // 接口返回信息 { ... }, ]; nock(API_HOST)// 攔截請求返回假定的response .get(`/api/enterprise/codingcorp/billings`) .reply(200, { code: 0, data }) return store.dispatch(actions.getAllBillings()) .then(() => { expect(store.getActions()).toMatchSnapshot(); }); }); });
expect(store.getActions()).toEqual({data ...});
這樣,你須要把 equal 裏的東西都想具體描寫清楚,而 toMatchSnapshot 可在當前目錄下生成一個 snapshot 存放這個當前結果,寫測試時看一眼結果是預期的就能夠 commit。若是改壞了函數就不匹配 snapshot 了。咱們寫的不少 component 是 extends component 的 jsx,測試這類須要一個 mock component 的工具庫 Enzyme 。api
it('should add key with never expire', () => { ... 掛載咱們的dom const wrapper = shallow( <TwoFactorModal verifyKey={verifyKeySpy} onVerifySuccess={onVerifySuccessSpy} /> ); // wrapper的setstate方法 wrapper.setState({ name: 'test', password: '123', }); const name = 'new name'; const content = 'new content'; const expiration = '2016-01-01'; wrapper.find('.name').simulate('change', {}, name); wrapper.find('.content').simulate('change', {}, content); expect(wrapper.find('.permanentCheck').prop('checked')).toBe(true); // 此處也可使用toMatchSnapshot // submit to add wrapper.find('.submitBtn').simulate('click', e); return promise.then(() => { expect(onCheckSuccess).toBeCalledWith({ name, password, }); }); });
Enzyme 給咱們提供了不少 react-dom 的事件操做與數據獲取。
這類 component 的測試通常分爲
- Structural Testing 結構測試
主要關心一個界面是否有這些元素
例如咱們有一個界面是
結構化測試將包含:
- 一個 title 包含 「登入到 codingcorp.coding.net」
- 一個副標題包含 「..」
- 兩個輸入框
- 一個提交按鈕
...
比較方便的實現就是利用 jest 的 snapshot 測試方法,先作一個預期生成 snapshot,以後的版本與預期對比。
UI 的樣式測試爲了測試咱們的樣式是否複合設計稿預期。同時經過樣式測試咱們能夠感覺當咱們 code 變化帶來的 ui 變化,以及他是否符合預期。
若是樣式是 inline style,這類測試其實直接使用 jest 的 Snapshot Testing 最方便,通常在組件庫中使用。
這部分其實屬於 E2E 測試中的部分,這裏提早講,主要解決的問題是咱們寫出來的 ui 是否符合設計稿的預期。咱們使用 BackstopJS 他的原理是經過對頁面的 viewports 和 scenarios 等作配置,利用 web-driver 獲取圖片,與設計稿或者預期圖作 diff,產生報告。
{
// 須要測試的模塊元素定義
"viewports": [
{ "name": "password", //密碼框 "width": 320, "height": 480 }, ], "scenarios": [ { "label": "members", "url": "/member/admin", "selectors": [ // css選擇器 ".member-selector" ], "readyEvent": "gmapResponded", "delay": 100, "misMatchThreshold" : 1, "onBeforeScript": "onBefore.js", "onReadyScript": "onReady.js" } ], "paths": { "bitmaps_reference": "backstop_data/bitmaps_reference", "bitmaps_test": "backstop_data/bitmaps_test", "html_report": "backstop_data/html_report", "ci_report": "backstop_data/ci_report" }, "casperFlags": [], "engine": "slimerjs", "report": ["browser"], "debug": false }
E2E 測試是在實際生產環境測試整個 app,一般來講這部分工做會讓測試人工作,並在實體環境跑,就像用戶實際在操做同樣。靠人工作遇到項目邏輯比較複雜,則須要每個版本都要測不少邏輯,擔憂提交一個影響了其餘部分。其實也有比較好的自動化跑腳本方案能幫助測試,咱們使用 selenium-webdriver 工具配合 async await 進行自動化 E2E 測試。
const {prepareDriver, cleanupDriver} = require('../utils/browser-automation')
//...
describe('member', function () {
let driver
...
before(async () => {
driver = await prepareDriver() }) after(() => cleanupDriver(driver)) it('should work', async function () { const submitBtn = await driver.findElement(By.css('.submitBtn')) await driver.get('http://localhost:4000') await retry(async () => { const displayElement = await driver.findElement(By.css('.display')) const displayText = await displayElement.getText() expect(displayText).to.equal('0') }) await submitBtn.click() })
selenium-webdriver 提供了不少瀏覽器的操做以及對元素對查找方法,以及元素內容的獲取方法,好比這裏的 By.css 選擇器。
有時候用戶端的設備很不一致,須要在不一樣設備上的匹配,因而咱們能夠用 selenium-webdriver 搭配 sourcelab 的設備牆進行
測試覆蓋率表達本次測試有有多少比例的語句,函數分支沒有被測到。固然絕對數字做爲代碼質量依據並無什麼意義,由於它是根據咱們寫的測試來的。卻是學習爲何有些代碼沒有被覆蓋到,以及爲何有些代碼變了測試卻沒有失敗。頗有意義。咱們在 jestconfig 中配置完目標數據後,每次他會檢測咱們的測試覆蓋率並給咱們報告
顧名思義,就是指這個函數是否被測試代碼調用了。如下面的代碼爲例
,對函數 exchange 要作到覆蓋,只要一個測試——如 expect(exchange(2, 2)) 就能夠了。若是連函數覆蓋都達不到,那這個函數是否真的須要。
let z = 0
if (x>0 && y>0) {
z=x } return z }
仍是前面那個 exchange 例子,他檢測的是某一行代碼是否被測試覆蓋了,一樣 選擇用例 2,2 也能覆蓋它,可是若是變成 2, -1 就不行了。一般這種狀況是因爲一些分支語句致使的,由於相應的問題就是 「那行代碼(以及它所對應的分支)須要嗎?
它是指每個邏輯分支是否被測試覆蓋了,有一個 if 的真和假通常就要兩組用例,至少測一組 true 一組 false
它是指分支中的每一個條件是否被測試覆蓋了,像前面那個 exchange 例子,要達到所有條件覆蓋,測試用例就須要四個,即 x 和 y 四種狀況,若是測不到就要思考是否不須要某個分支呢
說到這裏從新提一下 jest 的 toMatchSnapshot 實踐,他對指望的表達並非寫一個指望值和實際作匹配,而是生成一個快照讓咱們以後的每次變異代碼和它匹配, jest--watch 的實時測試變更的代碼更方便作這個事。
這裏所謂的變異是指修改一處代碼來改變代碼的行爲,檢查測試是否由於這個代碼的變異而失敗,若是有失敗則說明這個變異被消滅,此時的測試自己行爲是符合預期。否則變異存活則測試不到位。
平時用到比較多的變異方法是:
條件邊界變異、反向條件變異、數學運算變異、增量運算變異、負值翻轉變異等
養成寫測試的好習慣能避免不少問題,極大的提高效率,避免重複 debug。在前端開發中因爲語言自己對寫法限制比較弱,測試保障很是重要,既讓本身對代碼有信心也讓別人更容易理解你設計的每個模塊用意。在寫代碼的時候就要從可測試如何測試的角度思考,儘可能每一行代碼都是有用且符合預期的。