測試
隨著代碼量的增長 細(xì)小的錯誤和一些沒能夠預(yù)料到的邊界條件能夠造成大的問題
bug可以造成糟糕的用戶體驗 甚至是業(yè)務(wù)的損失
一個有效的監(jiān)測方式是在發(fā)布前對代碼進行測試
為什么要做測試
是人都會犯錯。測試可以幫助我們發(fā)現(xiàn)問題并驗證代碼的正確與否。測試可以保證代碼在新增功能、重構(gòu)代碼或者升級了依賴后繼續(xù)正常工作
測試的價值遠(yuǎn)比我們意識到的要多很多。最好的修復(fù)bug的方式就是寫一個失敗的用例來報錯出來 在修復(fù)bug的過程中重新跑測試用例,如果它通過了意味著bug修復(fù)了
測試用例還能作為團隊的文檔,幫助未見過代碼庫的人來理解現(xiàn)有代碼是如何工作的。
更多的自動化測試意味著花費在人工測試的時間上更少,從而釋放出寶貴的時間
一. 策略
1. 單元測試
1.1 概念
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。
最小的可測但愿可以是一個模塊、一個函數(shù)或者一個類
單元測試從長期來看,可以提高代碼質(zhì)量,減少維護成本,降低重構(gòu)難度。但是從短期來看,加大了工作量,對于進度緊張的項目中的開發(fā)人員來說,可能會成為不少的負(fù)擔(dān)
1.2 哪些代碼需要有單元測試覆蓋
- 邏輯復(fù)雜的
- 容易出錯的
- 不易理解的,即使是自己過段時間也會遺忘的,看不懂自己的代碼,單元測試代碼有助于理解代碼的功能和需求
- 公共代碼。比如自定義的所有http請求都會經(jīng)過的攔截器;工具類等。
- 核心業(yè)務(wù)代碼。一個產(chǎn)品里最核心最有業(yè)務(wù)價值的代碼應(yīng)該要有較高的單元測試覆蓋率。
1.3 何時寫
在具體實現(xiàn)代碼之前,這是測試驅(qū)動開發(fā)(TDD)所提倡的;
與具體實現(xiàn)代碼同步進行。先寫少量功能代碼,緊接著寫單元測試(重復(fù)這兩個過程,直到完成功能代碼開發(fā))。其實這種方案跟第一種已經(jīng)很接近,基本上功能代碼開發(fā)完,單元測試也差不多完成了。
編寫完功能代碼再寫單元測試
1.4 為什么要寫單元測試
- 修改代碼時,可以很快的驗證正確性
- 重構(gòu)代碼時,只要原有測試用例全部通過,則代碼重構(gòu)完成,當(dāng)然需要原有測試用例設(shè)計合理
- 當(dāng)功能持續(xù)增加時,如果影響到原有功能,可以很快發(fā)現(xiàn)
1.5小結(jié)
- 單元測試可以有效地測試某個程序模塊的行為,是未來重構(gòu)代碼的信心保證。
- 單元測試的測試用例要覆蓋常用的輸入組合、邊界條件和異常。
- 單元測試代碼要非常簡單,如果測試代碼太復(fù)雜,那么測試代碼本身就可能有bug。
- 單元測試通過了并不意味著程序就沒有bug了,但是不通過程序肯定有bug。
集成測試
接下來是集成測試。集成測試是一組不同的單元被當(dāng)作一個整體來進行測試的階段。
又稱組裝測試,即對程序模塊采用一次性或增值方式組裝起來,對系統(tǒng)的接口進行正確性檢驗的測試工作。整合測試一般在單元測試之后、系統(tǒng)測試之前進行。實踐表明,有時模塊雖然可以單獨工作,但是并不能保證組裝起來也可以同時工作。
功能測試
對系統(tǒng)的功能及性能的總體測試。
是一種黑盒測試的類型,其主要關(guān)注于用戶需求和交互。功能測試會從整體上覆蓋所有的底層軟件,所有的用戶交互和應(yīng)用。
測試金字塔
在金字塔模型之前,流行的是冰淇淋模型。包含了大量的手工測試、端到端的自動化測試及少量的單元測試。造成的后果是,隨著產(chǎn)品壯大,手工回歸測試時間越來越長,質(zhì)量很難把控;自動化case頻頻失敗,每一個失敗對應(yīng)著一個長長的函數(shù)調(diào)用,到底哪里出了問題?單元測試少的可憐,基本沒作用。

)
Mike Cohn 在他的著作《Succeeding with Agile》一書中提出了“測試金字塔”這個概念。這個比喻非常形象,它讓你一眼就知道測試是需要分層的。它還告訴你每一層需要寫多少測試。 測試金字塔本身是一條很好的經(jīng)驗法則,我們最好記住Cohn在金字塔模型中提到的兩件事:

是目前比較流行的一種設(shè)計自動化測試的思路,核心觀點如下:
- 越下層的測試效率越高, 覆蓋率也越高, 開發(fā)維護成本越低
- 更上層的測試集成性更好, 但維護成本更高
- 大量的Unit測試, 少量的集成測試和更少的E2E測試是比較合理的平衡點(Google在blog中推薦70/20/10的測試用例個數(shù)比例)
React Native
端到端測試 End-to-End Tests
從用戶的視角驗證app功能的正確性
這是在編譯app包之后,對安裝包進行測試,此時不再是考慮React組件 React Native API或者是Redux store或者業(yè)務(wù)邏輯。
E2E測試庫允許找出屏幕上的控件 并且可以與其進行交互
E2E測試可以給app最大的信心,與其它測試相比
- 編寫測試用例需要花費更多的時間
- 運行更慢
- 更容易不穩(wěn)定("不穩(wěn)定"是指在沒有任何更改的情況下隨機通過和失敗的測試)
在React Native中, Detox是一個流行的框架,因為它是為React Native應(yīng)用量身定制的。在iOS和Android應(yīng)用中,Appium也是一個流行框架
組件測試
React組件負(fù)責(zé)渲染app,用戶直接和組件進行交互。及時app業(yè)務(wù)邏輯有很高的測試覆蓋率并且是正常工作的,沒有組件測試,可能還是會給用戶展示一個有問題的頁面。組件測試可以分成單元測試和集成測試,它們是React Native核心部分,我們分別來講
對于React組件 有兩個東西需要測試
- 交互:確保組件和用戶交互時是正常運行的(比如 當(dāng)用戶點擊某個按鈕時)
- 渲染:確保React組件顯示是正確的(比如 按鈕在頁面的外觀以及位置)
比如,想測試按鈕的顯示以及點擊時間是否正確被處理
React的Test Render可以將React組件轉(zhuǎn)化成純Javascript對象,不需要依賴DOM或者是移動手機環(huán)境
React Native Testing Library 基于React的Test Renderer還添加了fireEvent和query API
組件測試只是Javascript測試,任何iOS、Android或React Native的代碼并不能被測試。也就是說不能保證100%的可用性。如果iOS或者Android代碼里有bug,是測試不出問題的
單元測試 framework
| 框架 | 特點 | 支持 | github stars |
|---|---|---|---|
| Jest | 配置簡單 并行運行 | React & React Native | 33k |
| Mocha | 靈活的測試框架,需要引入斷言庫(should.js, chai, expect.js, better-assert, unexpected等)、覆蓋統(tǒng)計等 | node.js & browser | 19.9k |
| Jasmine | 內(nèi)置斷言expect, 需要全局聲明,且需要配置,相對來說使用更復(fù)雜、不夠靈活。 |
node.js & browser | 15k |
| cypress | 提供可交互頁面 支持Mac、Windows、Linux | browser | 24.3k |
Jest
FaceBook出品的前端測試框架,適合用于React和React Native的單元測試。
有以下幾個特點:
簡單易用:易配置,自帶斷言庫和mock庫。
快照測試:能夠創(chuàng)造一個當(dāng)前組件的渲染快照,通過和上次保存的快照進行比較,如果兩者不匹配說明測試失敗。
測試報告:內(nèi)置了Istanbul,通過一定配置可以測試代碼覆蓋率,生成測試報告。
Enzyme
Enzyme是AirBnb開源的React測試工具庫,通過一套簡潔的api,可以渲染一個或多個組件,查找元素,模擬元素交互(如點擊,觸摸),通過和Jest相互配合可以提供完整的React組件測試能力。
初始化配置
jest --init
常用方法
describe:創(chuàng)造一個塊,將一組相關(guān)的測試用例組合在一起test:也可以用it,測試用例expect:使用該函數(shù)斷言某個值
生命周期
beforeAll
afterAll
beforeEach
afterEach
當(dāng)before和after在describe方法中時 作用域在describe里
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
describe執(zhí)行順序
describe('outer', () => {
console.log('describe outer-a');
describe('describe inner 1', () => {
console.log('describe inner 1');
test('test 1', () => {
console.log('test for describe inner 1');
expect(true).toEqual(true);
});
});
console.log('describe outer-b');
test('test 1', () => {
console.log('test for describe outer');
expect(true).toEqual(true);
});
describe('describe inner 2', () => {
console.log('describe inner 2');
test('test for describe inner 2', () => {
console.log('test for describe inner 2');
expect(false).toEqual(false);
});
});
console.log('describe outer-c');
});
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2
斷言
普通斷言
- toBe
- toEqual
- .not.toBe
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
})
真
- expect(n).toBeNull();
- expect(n).toBeDefined();
- expect(n).not.toBeUndefined();
- expect(n).not.toBeTruthy();
- expect(n).toBeFalsy();
數(shù)字相關(guān)的斷言
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
-
expect(value).toBeLessThanOrEqual(4.5);
//toBe and toEqual are equivalent for numbers expect(value).toBe(4);
expect(value).toEqual(4);
浮點數(shù)
test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});
字符串
可使用正則表達(dá)式
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
數(shù)組
- toContain
異常
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// You can also use the exact error message or a regexp
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
});
Mock 函數(shù)
- 替換真實的函數(shù)實現(xiàn)
- 檢測函數(shù)調(diào)用以及傳入的參數(shù)
- 測試時配置返回值
- 監(jiān)測實例的初始化
1. 在測試代碼中創(chuàng)建一個mock函數(shù)
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
// foo.js
module.exports = function () {
// some implementation;
};
// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
2. 手動Mock
在mocks下實現(xiàn)一份mock代碼
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views
3. require actual implement
jest.mock('node-fetch');
import fetch from 'node-fetch';
const {Response} = jest.requireActual('node-fetch');
Snapshot Test
初衷 測試React組件
確保UI不會意外更改時,是非常有用的工具
典型的例子是渲染UI組件后后,獲得組件快照,然后和之前存儲的snapshot文件進行比較
snapshot測試是傳統(tǒng)測試的一個補充而非替代
如何做snapshot測試
如果渲染真實的UI界面,需要編譯一個完整的app
如果想要單獨的測試一個組件 可以使用測試渲染工具來快速生成React樹的可序列化的值
-
demo test
// __tests__/Intro-test.js import React from 'react'; import renderer from 'react-test-renderer'; import Intro from '../Intro'; test('renders correctly', () => { const tree = renderer.create(<Intro />).toJSON(); expect(tree).toMatchSnapshot(); }); -
Demo result
// __tests__/__snapshots__/Intro-test.js.snap exports[`Intro renders correctly 1`] = ` <View style={ Object { "alignItems": "center", "backgroundColor": "#F5FCFF", "flex": 1, "justifyContent": "center", } }> <Text style={ Object { "fontSize": 20, "margin": 10, "textAlign": "center", } }> Welcome to React Native! </Text> <Text style={ Object { "color": "#333333", "marginBottom": 5, "textAlign": "center", } }> This is a React Native snapshot test. </Text> </View> `; -
.snap如何產(chǎn)生
.snap組件快照是一串類似JSX的字符串,由Jest內(nèi)置的React序列化器生成
將React組件樹轉(zhuǎn)換成方便人閱讀的形式,也就是說組件快照是是組件渲染結(jié)果的文本形式第一次運行測試的時候會將當(dāng)前渲染結(jié)果生成snapshot 之后每次運行測試都是與此次的snap文件進行比較
-
CI里是否可以自動生成
Jest執(zhí)行測試是不會自動生成 通過--updateSnapshot來強制生成 snapshot文件是否放在git等版本管理中
是的
Jest使用pretty-format來生成可讀的snapshots文件,使用 snapshot-diff來比較組件snapshot文件的差異
Snapshot 測試失敗

- 界面出問題了
- 新的snapshot是所希望的結(jié)果 則需要更新snapshot
此時測試結(jié)果里會展示新舊.snap的不同之處
異步測試

測試覆蓋率

class Mock
describe('DestinationClass', () => {
it('mock function of the instance of destination class', () => {
const instanceFunction = jest.fn()
DestinationClass.prototype.instanceFunction = instanceFunction
const destinationInstance = new DestinationClass()
destinationInstance.instanceFunction()
expect(instanceFunction).toHaveBeenCalledTimes(1)
})
it('mock static function of destination class', () => {
const classFunction = jest.fn()
DestinationClass.classFunction = classFunction
DestinationClass.classFunction()
expect(classFunction).toHaveBeenCalledTimes(1)
})
})
單純網(wǎng)絡(luò)API功能測試
- SoapUI
- JMeter
- PostMan
- 自己寫代碼
app內(nèi)mock參考
測試結(jié)果的可視化
jest-html-reporter
npm install jest-html-reporter --save-dev
reporters: [
'default',
[
'./node_modules/jest-html-reporter',
{
pageTitle: 'Test Report',
},
],
],
默認(rèn)輸出結(jié)果在./test-report.html中
測試報告與Sonarquebe集成
Jest 實操
全局mock
jestSetupFiles.js
import mockAsyncStorage from '@react-native-community/async-storage/jest/async-storage-mock'
jest.mock('@react-native-community/async-storage', () => mockAsyncStorage)
實踐中遇到的問題
- test時控制臺有l(wèi)og, 如何取消log
global.console = {
log: jest.fn(), // console.log are ignored in tests
// Keep native behaviour for other methods, use those to print out things in your own tests, not `console.log`
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
}
-
Native module cannot be null
jest.mock('react-native-device-info') -
Cannot find module './images/ic_default_avatar.png'
require('./images/ic_default_avatar.png')*require('img1.png') becomes
Object { "testUri": 'path/to/img1.png' }in the Jest snapshot.增加assetFileTransformer.js文件并在jest.config.js中添加
moduleNameMapper: { // '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__tests__/__mocks__/assetFileTransformer.js', },