測試分爲e2e測試和單元測試和集成測試html
e2e:端到端的測試,主要是測業務,絕大部分狀況是指在瀏覽器上對某一個網站的某一個功能進行操做。前端
單元測試工具:mache、ava、jest、jasmine等vue
斷言庫: shoud.js.chai.js 等node
測試覆蓋率工具:istanbulreact
react 採用jest加enzyne的寫法 e2e 測試pupertearandroid
vue 採用jest e2e 適應nightwatch 的方案正則表達式
單元測試npm
在計算機編程中,單元測試(英語:Unit Testing)又稱爲模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工做。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法集成測試編程
集成測試,也叫組裝測試或聯合測試。在單元測試的基礎上,將全部模塊按照設計要求(如根據結構圖)組裝成爲子系統或系統,進行集成測試。功能測試json
功能測試就是對產品的各功能進行驗證,根據功能測試用例,逐項測試,檢查產品是否達到用戶要求的功能。
React & Redux 應用構建在三個基本的構建塊上:actions、reducers 和 components。是獨立測試它們(單元測試),仍是一塊兒測試(集成測試)取決於你。集成測試會覆蓋到整個功能,能夠把它想成一個黑盒子,而單元測試專一於特定的構建塊。從個人經驗來看,集成測試很是適用於容易增加但相對簡單的應用。另外一方面,單元測試更適用於邏輯複雜的應用。儘管大多數應用都適合第一種狀況,但我將從單元測試開始更好地解釋應用層。
vue中直接選就能夠
在其餘的項目中,直接測試就能夠
npm install --save-dev jest
在package.json中添加
// 添加測試命令 { "scripts": { "test": "jest" } }
執行命令
npm test
Jest 的測試腳本名形如.test.js,不論 Jest 是全局運行仍是經過npm test運行,它都會執行當前目錄下全部的.test.js 或 *.spec.js 文件、完成測試
一、相等匹配
expact(2 + 2) 將返回咱們指望的結果, toBe 就是一個matcher
test('two plus two is four', () => { expect(2 + 2).toBe(4); });
toBe 是測試具體的某一個值,若是須要測試對象,須要用到toEqual,toEqual是經過遞歸檢查對象或數組的每一個字段。
test('object assignment', () => { const data = {one: 1}; data['two'] = 2; expect(data).toEqual({one: 1, two: 2}); });
二、真實性匹配,好比:對象是否爲null,集合是否爲空等等
在測試中,您有時須要區分undefined、null和false,但有時但願以不一樣的方式處理這些問題,Jest幫助你明確您想要什麼。好比:
三、數字型匹配
test('two plus two', () => { const value = 2 + 2; 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); });
於float類型的浮點數計算的時候,須要使用toBeCloseTo而不是 toEqual ,由於避免細微的四捨五入引發額外的問題
四、字符型匹配 toMatch 匹配規則,支持正則表達式匹配
test('there is no I in team', () => { expect('team').not.toMatch(/I/); }); test('but there is a "stop" in Christoph', () => { expect('Christoph').toMatch(/stop/); });
五、數組類型匹配 toContain 檢查是否包含
const shoppingList = [ 'diapers', 'kleenex', 'trash bags', 'paper towels', 'beer', ]; test('the shopping list has beer on it', () => { expect(shoppingList).toContain('beer'); });
六、異常匹配 測試function是否會拋出特定的異常信息,能夠用 toThrow 規則
function compileAndroidCode() { throw new ConfigError('you are using the wrong JDK'); } test('compiling android goes as expected', () => { expect(compileAndroidCode).toThrow(); expect(compileAndroidCode).toThrow(ConfigError); // You can also use the exact error message or a regexp expect(compileAndroidCode).toThrow('you are using the wrong JDK'); expect(compileAndroidCode).toThrow(/JDK/); });
一、回調函數
done() 被執行則意味着callback函數被調用
function fetchData(callback) { setTimeout(() => { callback('2') }, 2000) } test('data is 2', done => { function callback(data) { expect(data).toBe('2'); done(); } fetchData(callback) })
二、promise驗證
assertions(1)表明的是在當前的測試中至少有一個斷言是被調用的,不然斷定爲失敗。
在Jest 20.0.0+ 的版本中你可使用 .resolves 匹配器在你的expect語句中,Jest將會等待一直到承諾被實現,若是承諾沒有被實現,測試將自動失敗。
若是你指望你的承諾是不被實現的,你可使用 .rejects ,它的原理和 .resolves相似
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('2') }, 2000) }) } test('data is 2', () => { expect.assertions(1); return expect(fetchData()).resolves.toBe('2'); }) test('data is 2', () => { expect.assertions(1); return expect(fetchData()).rejects.toMatch('error'); });
三、使用 Async/Await
function fetchData(num) { return new Promise((resolve, reject) => { setTimeout(() => { if(num) { reject('error') } else { resolve('2') } }, 2000) }) } test('data is 2', () => { expect.assertions(1); return expect(fetchData()).resolves.toBe('2'); }) test('the data is 2', async () => { expect.assertions(1); const data = await fetchData(); expect(data).toBe('2'); }); test('the fetch fails with an error', async () => { expect.assertions(1); try { await fetchData(1); } catch (e) { expect(e).toMatch('error'); } });
固然你也能夠將Async Await和 .resolves .rejects 結合起來(Jest 20.0.0+ 的版本)
test('the data is peanut butter', async () => { expect.assertions(1); await expect(fetchData()).resolves.toBe('peanut butter'); }); test('the fetch fails with an error', async () => { expect.assertions(1); await expect(fetchData()).rejects.toMatch('error'); });
在寫單元測試的時候有一個最重要的步驟就是Mock,咱們一般會根據接口來Mock接口的實現,好比你要測試某個class中的某個方法,而這個方法又依賴了外部的一些接口的實現,從單元測試的角度來講我只關心我測試的方法的內部邏輯,我並不關注與當前class自己依賴的實現,因此咱們一般會Mock掉依賴接口的返回,由於咱們的測試重點在於特定的方法,因此在Jest中一樣提供了Mock的功能
Jest中有兩種方式的Mock Function,一種是利用Jest提供的Mock Function建立,另一種是手動建立來覆寫自己的依賴實現。
一、 jest.fn() 方式
every
function every(array, predicate) { let index = -1 const length = array == null ? 0 : array.length while (++index < length) { if (!predicate(array[index], index, array)) { return false } } return true } module.exports = every
foreach
function foreach(arr, fn) { for(let i = 0, len = arr.length; i < len; i++) { fn(arr[i]); } } module.exports = foreach;
const foreach = require('./foreach'); const every = require('./every'); describe('mock test', () => { it('test foreach use mock', () => { // 經過jest.fn() 生成一個mock函數 const fn = jest.fn(); foreach([1, 2, 3], fn); // 測試mock函數被調用了3次 expect(fn.mock.calls.length).toBe(3); // 測試第二次調用的函數第一個參數是3 expect(fn.mock.calls[2][0]).toBe(3); }) it('test every use mock return value', () => { const fn = jest.fn(); // 能夠設置返回值 fn .mockReturnValueOnce(true) .mockReturnValueOnce(false); const res = every([1, 2, 3, 4], fn); expect(fn.mock.calls.length).toBe(2); expect(fn.mock.calls[1][1]).toBe(1); }) it('test every use mock mockImplementationOnce', () =>{ // 快速定義mock的函數體,方便測試 const fn = jest.fn((val, index) => { if(index == 2) { return false; } return true; }); const res = every([1, 2, 3, 4], fn); expect(fn.mock.calls.length).toBe(3); expect(fn.mock.calls[1][1]).toBe(1); }) })
二、手動
假如個人測試文件sum2.js
function sum2(a, b) { if (a > 10) return a * b; return a + b; }
export default sum2;
如今若是咱們要mock sum2.js 文件的話,須要在sum2.js 同級目錄下新建文件夾__mock__,
而後在此文件下新建文件同名 sum2.js, 只是單純的返回100
export default function sum2(a, b) { return 100; } 測試用例mock_file.test.js jest.mock('../src/sum2'); import sum2 from '../src/sum2'; it('test mock sum2', () => { // 由於此時訪問的是__mock__文件夾下的sum2.js 因此測試經過 expect(sum2(1, 11111)).toBe(100); })
手動mock的好處是測試和模擬分離。能夠很方便的修改測試用例。若是是複雜的mock建議使用手動新建文件方式
class Hook { constructor() { this.init(); } init() { this.a = 1; this.b = 1; } sum() { return this.a + this.b; } } describe('hook', () => { const hook = new Hook; // 每一個測試用例執行前都會還原數據,因此下面兩個測試能夠經過。 beforeEach( () => { hook.init(); }) test('test hook 1', () => { hook.a = 2; hook.b = 2; expect(hook.sum()).toBe(4); }) test('test hook 2', () => { expect(hook.sum()).toBe(2);// 測試經過 }) })
describe(name, fn)
describe(name, fn)建立一個塊,在一個「測試套件」中,將幾個相關的測試組合在一塊兒
const myBeverage = { delicious: true, sour: false, }; describe('my beverage', () => { test('is delicious', () => { expect(myBeverage.delicious).toBeTruthy(); }); test('is not sour', () => { expect(myBeverage.sour).toBeFalsy(); }); });
這不是必需的——你能夠直接在頂層編寫測試塊。可是,若是您但願將測試組織成組,那麼這就很方便了
describe.only(name, fn)
若是你只想運行一次模塊測試的話你可使用 only
describe.only('my beverage', () => { test('is delicious', () => { expect(myBeverage.delicious).toBeTruthy(); }); test('is not sour', () => { expect(myBeverage.sour).toBeFalsy(); }); }); describe('my other beverage', () => { // ... will be skipped });
describe.skip(name, fn) describe 等價於 xdescribe
你可使用skip 跳過某一個測試
describe('my beverage', () => { test('is delicious', () => { expect(myBeverage.delicious).toBeTruthy(); }); test('is not sour', () => { expect(myBeverage.sour).toBeFalsy(); }); }); describe.skip('my other beverage', () => { // ... will be skipped });
使用跳過一般只是一種比較簡單的替代方法,若是不想運行則能夠暫時將大量的測試註釋掉。
require.requireActual(moduleName)
返回實際的模塊而不是模擬,繞過全部檢查模塊是否應該接收模擬實現。
require.requireMock(moduleName)
返回一個模擬模塊,而不是實際的模塊,繞過全部檢查模塊是否正常。
test(name, fn, timeout) 等價於 it(name, fn, timeout)
在測試文件中,您所須要的是運行測試的測試方法。例如,假設有一個函數inchesOfRain()應該是零。你的整個測試能夠是:
test('did not rain', () => { expect(inchesOfRain()).toBe(0); });
第一個參數是測試名稱;第二個參數是包含測試指望的函數。第三個參數(可選)是超時(以毫秒爲單位),用於指定在停止前等待多長時間。注意:默認的超時是5秒。
注意:若是測試返回了一個promise,Jest會在測試完成以前等待promise。Jest還將等待,若是你爲測試函數提供一個參數,一般稱爲done。當你想要測試回調時,這將很是方便。請參見如何在此測試異步代碼。
test.only(name, fn, timeout)等同於 it.only(name, fn, timeout) or fit(name, fn, timeout)
test.skip(name, fn)等同於it.skip(name, fn) or xit(name, fn) or xtest(name, fn)
當您維護一個大型的代碼庫時,您可能有時會發現因爲某種緣由而臨時中斷的測試。
若是您想跳過這個測試,可是您不想僅僅刪除這個代碼,您可使用skip指定一些測試來跳過。
test('it is raining', () => { expect(inchesOfRain()).toBeGreaterThan(0); }); test.skip('it is not snowing', () => { expect(inchesOfSnow()).toBe(0); });
只有「it is raining」測試運行,由於另外一個測試運行test . skip。 您能夠簡單地對測試進行註釋,可是使用skip會更好一些,由於它將保持縮進和語法突出。
Jest 內置了測試覆蓋率工具istanbul,要開啓,能夠直接在命令中添加 --coverage 參數,或者在 package.json 文件進行更詳細的配置。
快照測試第一次運行的時候會將被測試ui組件在不一樣狀況下的渲染結果保存一份快照文件。後面每次再運行快照測試時,都會和第一次的比較。
import React from 'react'; export default class RC extends React.Component { render() { return ( <div>我是react組件 </div> ) } }
import React from 'react'; import renderer from 'react-test-renderer'; import RC from '../src/react-comp'; test('react-comp snapshot test', () => { const component = renderer.create(<RC />); // let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }) test('react-comp snapshot test2', () => { const component = renderer.create(<RC />); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); })
執行測試命令,會在test目錄下生成一個__snapshots__目錄,在此目錄下會與一個文件叫snapshot.test.js.snap的快照文件
使用
npm install -g jest-codemods 而後jest-codemods
shallow() 渲染函數只渲染咱們專門測試的組件, 它不會渲染子元素。相反, 用mount()
一、使用 shallow
mport { shallow } from 'enzyme'; const wrapper = shallow(<MyComponent />);
咱們剛剛能夠看到這個測試裏用到shallow函數,它支持對DOM的進行結構和事件的響應,若是你對jQuery比較熟悉的話,那麼你對它的語法也不會陌生。好比咱們測試裏用到的find方法,你們常常用它來尋找一些DOM數組。
簡單羅列下它所支持的方法:
二、徹底DOM渲染
import { mount } from 'enzyme'; const wrapper = mount(<MyComponent />);
徹底DOM渲染主要用於與DOM API進行交互以及須要完整生命週期的組件測試(i.e componentDidMoun)。徹底DOM渲染須要DOM 的 API 在全局做用域內。並且須要其運行在近似瀏覽器的環境裏。若是你不想在瀏覽器裏跑這些測試的話,強烈建議你使用mount,一個依賴於jsdom的類庫,幾乎等同於沒有瀏覽器外殼的瀏覽器。它也支持了不少方法
三、靜態渲染
靜態渲染,enzyme還提供了靜態渲染,將組件渲染成html,用於咱們分析html的結構。render相比前兩種用法, 主要是在於更換了類庫 Cheerio ,並且做者也相信在處理解析上會更好點。
import { render } from 'enzyme'; const wrapper = render(<MyComponent />);
若是咱們在開發過程當中就進行了測試(直接採用 TDD 開發模式、或者針對既有的模塊寫用例),
會有以下的好處:
固然,凡事都有兩面性,好處雖然明顯,卻並非全部的項目都值得引入測試框架,畢竟維護測試用例也是須要成本的。對於一些需求頻繁變動、複用性較低的內容,好比活動頁面,讓開發專門抽出人力來寫測試用例確實得不償失。而那些適合引入測試場景大概有這麼幾個:
由於多處複用,更要保障質量