1. 程式人生 > 程式設計 >前端Vue單元測試入門教程

前端Vue單元測試入門教程

目錄
  • 一、為什麼需要單元測試
  • 二、如何寫單元測試
  • 三、測試工具
  • 四、Jest入門
    • 安裝
    • 簡單示例
    • Jest Cli
    • 使用配置檔案
    • 使用 Babel
    • -cli 中使用 Jest
    • 常見示例
      • 判斷值相等
      • 檢查類false值
      • 數字大小比較
      • 字串比較
      • 陣列和類陣列
      • 異常
      • 只執行當前test
    • 測試非同步程式碼
      • 回撥函式
      • Promises
      • Async/Await
    • 安裝和拆卸
      • 測試前和測試後
      • 測試用例分組
      • 執行順序
    • mock 函式
      • 測試mock
      • mock的返回值
      • 模擬介面返回
      • mock函式的匹配器
  • 五、Vue Test Utils
    • 測試單檔案元件
      • 處理 webpack 別名
        • 掛載元件
          • 測試元件渲染出來的 HTML
            • 模擬使用者操作
              • 元件的事件
                • 元件的data
                  • 模擬vue例項方法
                    • 全域性外掛
                      • 測試watch
                        • 第三方外掛
                        • 六、總結

                          一、為什麼需要單元測試

                          單元測試是用來測試專案中的一個模組的功能,如函式、類、元件等。單元測試的作用有以下:

                          • 正確性:可以驗證程式碼的正確性,為上線前做更詳細的準備;
                          • 自動化:測試用例可以整合到程式碼版本管理中,自動執行單元測試,避免每次手工操作;
                          • 解釋性:能夠為其他開發人員提供被測模組的文件參考,閱讀測試用例可能比文件更完善;
                          • 驅動開發、指導設計:提前寫好的單元測試能夠指導開發的API設計,也能夠提前發現設計中的問題;
                          • 保證重構:測試用例可以多次驗證,當需要回歸測試時能夠節省大量時間。

                          二、如何寫單元測試

                          測試原則

                          • 測試程式碼時,只考慮測試,不考慮內部實現
                          • 資料儘量模擬現實,越靠近現實越好
                          • 充分考慮資料的邊界條件
                          • 對重點、複雜、核心程式碼,重點測試
                          • 測試、功能開發相結合,有利於設計和程式碼重構

                          編寫步驟

                          • 準備階段:構造引數,建立 spy 等
                          • 執行階段:用構造好的引數執行被測試程式碼
                          • 斷言階段:用實際得到的結果與期望的結果比較,以判斷該測試是否正常
                          • 清理階段:清理準備階段對外部環境的影響,移除在準備階段建立的 spy 等

                          三、測試工具

                          單元測試的工具可分為三類:

                          • 測試執行器(Test Runner):可以模擬各種瀏覽器環境,自定義配置測試框架和斷言庫等,如Karma.
                          • 測試框架:提供單元測試的功能模組,常見的框架有Jest,mocha,Jasmine,QUnit.
                          • 工具庫:assert,should.,expect.js,chai.js等斷言庫,enzyme渲染庫,Istanbul覆蓋率計算。

                          這裡,我們將使用 Jest 作為例子。Jest 功能全面,集成了各種工具,且配置簡單,甚至零配置直接使用。

                          四、Jest入門

                          Jest 官網的描述是這樣的:

                          Jest is a delightful Testing Framework with a focus on simplicity.

                          安裝

                          yarn add --dev jest
                          # or
                          # npm install -D jest
                          

                          簡單示例

                          從官網提供的示例開始,測試一個函式,這個函式完成兩個數字的相加,建立一個 sum.js 檔案︰

                          function sum(a,b) {
                            return a + b;
                          }
                          module.exports = sum;
                          

                          然後,建立 sum.test.js 檔案︰

                          const sum = require('./sum');
                          
                          test('adds 1 + 2 to equal 3',() => {
                            expect(sum(1,2)).toBe(3);
                          });
                          
                          package.json 裡增加一個測試任務:
                          {
                            "scripts": {
                              "test": "jest"
                            }
                          }
                          
                          

                          最後,執行 yarn test 或 npm run test ,Jest將列印下面這個訊息:

                          PASS ./sum.test.js
                          ✓ adds 1 + 2 to equal 3 (5ms)

                          至此,完成了一個基本的單元測試。

                          注意:Jest 通過用 JSDOM 在 Node 虛擬瀏覽器環境模擬真實瀏覽器,由於是用 js 模擬 DOM,所以 Jest 無法測試樣式 。Jest 測試執行器自動設定了 JSDOM。

                          Jest Cli

                          你可以通過命令列直接執行Jest(前提是jest已經加到環境變數PATH中,例如通過 yarn global add jest 或 npm install jest --global 安裝的 Jest) ,併為其指定各種有用的配置項。如:

                          jest my-test --notify --config=config.json
                          

                          Jest 命令有以下常見引數:

                          • --coverage 表示輸出單元測試覆蓋率,覆蓋率檔案預設在 tests/unit/coverage/lcov-report/index.html;
                          • --watch 監聽模式,與測試用例相關的檔案更改時都會重新觸發單元測試。

                          更多選項檢視Jest CLI Options.

                          使用配置檔案

                          使用 jest 命令可生成一個配置檔案:

                          jest --init
                          

                          過程中會有幾個選項供你選擇:

                          √ Would you like to use Typescript for the configuration file? ... no
                          √ Choose the test environment that will be used for testing jsdom (browser-like)
                          √ Do you want Jest to add coverage reports? ... yes
                          √ Which provider should be used to instrument code for coverage? babel
                          √ Automatically clear mock calls and instances between every test? ... yes

                          配置檔案示例(不是基於上述選擇):

                          // jest.config.js
                          const path = require('path')
                          
                          module.ehttp://www.cppcns.comxports = {
                              preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',rootDir: path.resolve(__dirname,'./'),coverageDirectory: '<rootDir>/tests/unit/coverage',collectCoverageFrom: [
                                  'src/*.{js,ts,vue}','src/directives/*.{js,'src/filters/*.{js,'src/helper/*.{js,'src/views/**/*.{js,'src/services/*.{js,vue}'
                              ]
                          }

                          使用 Babel

                          yarn add --dev babel-jest @babel/core @babel/preset-env
                          

                          可以在工程的根目錄下建立一個babel.config.js檔案用於配置與你當前Node版本相容的Babel:

                          // babel.config.js
                          module.exports = {
                            presets: [['@babel/preset-env',{targets: {node: 'current'}}]],};
                          

                          vue-cli 中使用 Jest

                          在專案中安裝 @vue/cli-plugin-unit-jest 外掛,即可在 vue-cli 中使用 Jest:

                          vue add unit-jest
                          # or
                          # yarn add -D @vue/cli-plugin-unit-jest @types/jest
                          
                          "scripts": {
                              "test:unit": "vue-cli-service test:unit --coverage"
                          },

                          @vue/cli-plugin-unit-jest 會在 vue-cli-service 中注入命令 test:unit,預設會識別以下檔案:<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)) 執行單元測試,即 tests/unit 目錄下的 .spec.(js|jsx|ts|tsx) 結尾的檔案及目錄名為 __tests__ 裡的所有 js(x)/ts(x) 檔案。

                          常見示例

                          判斷值相等

                          toBe() 檢查兩個基本型別是否精確匹配:

                          test('two plus two is four',() => {
                            expect(2 + 2).toBe(4);
                          });
                          
                          

                          toEqual() 檢查物件是否相等:

                          test('object assignment',() => {
                            const data = {one: 1};
                            data['two'] = 2;
                            expect(data).toEqual({one: 1,two: 2});
                          });

                          檢查類false值

                          • toBeNull 只匹配 null
                          • toBeUndefined 只匹配 undefined
                          • toBeDefined 與 toBeUndefined 相反
                          • toBeTruthy 匹配任何 if 語句為真
                          • toBeFalsy 匹配任何 if 語句為假

                          示例:

                          test('null',() => {
                            const n = null;
                            expect(n).toBeNull();
                            expect(n).toBeDefined();
                            expect(n).not.toBeUndefined();
                            expect(n).not.toBeTruthy();
                            expect(n).toBeFalsy();
                          });
                          
                          test('zero',() => {
                            const z = 0;
                            expect(z).not.toBeNull();
                            expect(z).toBeDefined();
                            expect(z).not.toBeUndefined();
                            expect(z).not.toBeTruthy();
                            expect(z).toBeFalsy();
                          });
                          
                          

                          數字大小比較

                          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);
                          });
                          
                          

                          對於比較浮點數相等,使用 toBeCloseTo 而不是 toEqual,因為你不希望測試取決於一個小小的舍入誤差。

                          test('兩個浮點數字相加',() => {
                            const value = 0.1 + 0.2;
                            //expect(value).toBe(0.3);           這句會報錯,因為浮點數有舍入誤差
                            expect(value).toBeCloseTo(0.3); // 這句可以執行
                          });
                          

                          字串比較

                          可以使用正則表示式檢查:

                          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','milk',];
                          
                          test('the shopping list has milk on it',() => {
                            expect(shoppingList).toContain('milk');
                            expect(new Set(shoppingList)).toContain('milk');
                          });

                          異常

                          還可以用來檢查一個函式是否丟擲異常:

                          function compileCode() {
                            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/);
                          });

                          更多使用方法參考API文件.

                          只執行當前test

                          可使用 only() 方法表示只執行這個test,減少不必要的重複測試:

                          test.only('it is raining',() => {
                            expect(inchesOfRain()).toBeGreaterThan(0);
                          });
                          
                          test('it is not snowing',() => {
                            expect(inchesOfSnow()).toBe(0);
                          });
                          
                          

                          測試非同步程式碼

                          回撥函式

                          例如,假設您有一個 fetchData(callback) 函式,獲取一些資料並在完成時呼叫 callback(data)。 你期望返回的資料是一個字串 'peanut butter':

                          test('the data is peanut butter',done => {
                            function callback(data) {
                              try {
                                expect(data).toBe('peanut butter');
                                done();
                              } catch (error) {
                                done(error);
                              }
                            }
                          
                            fetchData(callback);
                          });
                          
                          

                          使用 done() 是為了標識這個 test 執行完畢,如果沒有這個 done(),在 test 執行完畢後,我們的單元測試就結束了,這不符合我們的預期,因為callback還未呼叫,單元測試還沒走完。若 done() 函式從未被呼叫,將會提示超時錯誤。

                          若 expect 執行失敗,它會丟擲一個錯誤,後面的 done() 不再執行。 若我們想知道測試用例為何失敗,我們必須將 expect 放入 try 中,將 error 傳遞給 catch 中的 done 函式。 否則,最後控制檯將顯示一個超時錯誤失敗,不能顯示我們在 expect(data) 中接收的值。

                          Promises

                          還是使用上面的例子:

                          test('the data is peanut butter',() => {
                            return fetchData().then(data => {
                              expect(data).toBe('peanut butter');
                            });
                          });
                          

                          一定不要忘記 return 結果,這樣才能確保測試和功能同時結束。
                          如果是期望 Promise 被 reject,則使用 catch 方法:

                          test('the fetch fails with an error',() => {
                            expect.assertions(1);
                            return fetchData().catch(e => expect(e).toMatch('error'));
                          });
                          

                          還可以使用 resolves 和 rejects 匹配器:

                          test('the data is peanut butter',() => {
                            return expect(fetchData()).resolves.toBe('peanut butter');
                          });
                          
                          test('the fetch fails with an error',() => {
                            return expect(fetchData()).rejects.toMatch('error');
                          });
                          
                          

                          Async/Await

                          test('the data is peanut butter',async () => {
                            const data = await fetchData();
                            expect(data).toBe('peanut butter');
                          });
                          
                          test('the fetch fails with an error',async () => {
                            expect.assertions(1);
                            try {
                              await fetchData();
                            } catch (e) {
                              expect(e).toMatch('error');
                            }
                          });
                          
                          

                          async/await 還可以和 resolves()/rejects() 結合使用:

                          test('the data is peanut butter',async () => {
                            await expect(fetchData()).resolves.toBe('peanut butter');
                          });
                          
                          test('the fetch fails with an error',async () => {
                            await expect(fetchData()).rejects.toMatch('error');
                          });
                          
                          

                          安裝和拆卸

                          測試前和測試後

                          在某些情況下,我們開始測試前需要做一些準備工作,然後在測試完成後,要做一些清理工作,可以使用 beforeEach 和 afterEach。
                          例如,我們在每個test前需要初始化一些城市資料,test結束後要清理掉:

                          beforeEach(() => {
                            initializeCityDatabase();
                          });
                          
                          afterEach(() => {
                            clearCityDatabase();
                          });
                          
                          test('city database has Vienna',() => {
                            expect(isCity('Vienna')).toBeTruthy();
                          });
                          
                          test('city database has San Juan',() => {
                            expect(isCity('San Juan')).toBeTruthy();
                          });
                          
                          

                          類似的還有 beforeAll 和 afterAll,在當前spec測試檔案開始前和結束後的單次執行。

                          測試用例分組

                          預設情況下,before 和 after 的塊可以應用到檔案中的每個測試。 此外可以通過 describe 塊來將測試分組。 當 before 和 after 的塊在 describe 塊內部時,則其只適用於該 describe 塊內的測試。

                          // Applies to all tests in this file
                          beforeEach(() => {
                            return initializeCityDatabase();
                          });
                          
                          test('city database has Vienna',() => {
                            expect(isCity('San Juan')).toBeTruthy();
                          });
                          
                          describe('matching cities to foods',() => {
                            // Applies only to tests in this describe block
                            beforeEach(() => {
                              return initializeFoodDatabase();
                            });
                          
                            test('Vienna <3 sausage',() => {
                              expect(isValidCityFoodPair('Vienna','Wiener Wrstchen')).toBe(true);
                            });
                          
                            test('San Juan <3 plantains',() => {
                              expect(isValidCityFoodPair('San Juan','Mofongo')).toBe(true);
                            });
                          });
                          
                          

                          執行順序

                          由於使用了 describe 進行分組,於是就有了巢狀的作用域,各生命週期的執行順序如下:

                          • 外層作用域的 before 比內層的先執行,而 after 則相反;
                          • 同一層級 beforeAll 比 beforeEach 先執行,after 則相反;
                          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

                          mock 函式

                          jest.fn() 可以用來生成一個 mock 函式,jest 可以捕獲這個函式的呼叫、this、返回值等,這在測試回撥函式時非常有用。

                          測試mock

                          假設我們要測試函式 forEach 的內部實現,這個函式為傳入的陣列中的每個元素呼叫一次回撥函式。

                          function forEach(items,callback) {
                            for (let index = 0; index < items.length; index++) {
                              callback(items[index]);
                            }
                          }

                          為了測試此函式,我們可以使用一個 mock 函式,然後檢查 mock 函式的狀態來確保回撥函式如期呼叫。

                          const mockCallback = jest.fn(x => 42 + x);
                          forEach([0,1],mockCallback);
                          
                          // 此 mock 函式被呼叫了兩次
                          expect(mockCallback.mock.calls.length).toBe(2);
                          
                          // 第一次呼叫函式時的第一個引數是 0
                          expect(mockCallback.mock.calls[0][0]).toBe(0);
                          
                          // 第二次呼叫函式時的第一個引數是 1
                          expect(mockCallback.mock.calls[1][0]).toBe(1);
                          
                          // 第一次函式呼叫的返回值是 42
                          expect(mockCallback.mock.results[0].value).toBe(42);

                          mock的返回值

                          Mock 函式也可以用於在測試期間將測試值注入程式碼︰

                          const myMock = jest.fn();
                          console.log(myMock());
                          // > undefined
                          
                          myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
                          
                          console.log(myMock(),myMock(),myMock());
                          // > 10,'x',true,true
                          
                          

                          模擬介面返回

                          假定有個從 API 獲取使用者的類。 該類用 axios 呼叫 A客棧PI 然後返回 data,其中包含所有使用者的屬性:

                          // users.js
                          import axios from 'axios';
                          
                          class Users {
                            static all() {
                              return axios.get('/users.json').then(resp => resp.data);
                            }
                          }
                          
                          export default Users;
                          
                          

                          現在,為測試該方法而不實際呼叫 API (使測試緩慢與脆弱),我們可以用 jest.mock(...) 函式自動模擬 axios 模組。一旦模擬模組,我們可為 .get 提供一個 mockResolvedValue ,它會返回假資料用於測試。

                          // 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));
                          });
                          
                          

                          mock函式的匹配器

                          有了mock功能,就可以給函式增加一些自定義匹配器:

                          // The mock function was called at least once
                          expect(mockFunc).toHaveBeenCalled();
                          
                          // The mock function was called at least once with the specified args
                          expect(mockFunc).toHaveBeenCalledWith(arg1,arg2);
                          
                          // The last call to the mock function was called with the specified args
                          expect(mockFunc).toHaveBeenLastCalledWith(arg1,arg2);
                          
                          // All calls and the name of the mock is written as a snapshot
                          expect(mockFunc).toMatchSnapshot();
                          
                          也可以自己通過原生的匹配器模擬,下方的程式碼與上方的等價:
                          // The mock function was called at least once
                          expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
                          
                          // The mock function was called at least once with the specified args
                          expect(mockFunc.mock.calls).toContainEqual([arg1,arg2]);
                          
                          // The last call to the mock function was called with the specified args
                          expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
                            arg1,arg2,]);
                          
                          // The first arg of the last call to the mock function was `42`
                          // (note that there is no sugar helper for this specific of an assertion)
                          expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
                          
                          // A snapshot will check that a mock was invoked the same number of times,// in the same order,with the same arguments.
                          expect(mockFunc.mock.calls).toEqual([[arg1,arg2]]);
                          expect(mockFunc.getMockName()).toBe('a mock name');

                          五、Vue Test Utils

                          官網是這樣介紹 Vue Test Utils 的:

                          Vue Test Utils 是 Vue.js 官方的單元測試實用工具庫。

                          以下的例子均基於 vue-cli 腳手架,包括 webpack/babel/vue-loader

                          測試單檔案元件

                          Vue 的單檔案元件在它們運行於 Node 或瀏覽器之前是需要預編譯的。我們推薦兩種方式完成編譯:通過一個 Jest 預編譯器,或直接使用 webpack。這裡我們選用 Jest 的方式。

                          yarn add -D jest @vue/test-utils vue-jest
                          

                          vue-jest 目前並不支援 vue-loader 所有的功能,比如自定義塊和樣式載入。額外的,諸如程式碼分隔等 webpack 特有的功能也是不支援的。如果要使用這些不支援的特性,你需要用 Mocha 取代 Jest 來執行你的測試,同時用 webpack 來編譯你的元件。

                          處理 webpack 別名

                          vue-cli 中預設使用 @ 作為 /src 的別名,在 Jest 也需要單獨配置:

                          // jest.config.js
                          
                          module.exports = {
                              moduleNameMapper: {
                                  '^@/(.*)$': '<rootDir>/src/$1'
                              }
                          }
                          
                          

                          掛載元件

                          被掛載的元件會返回到一個包裹器內,而包裹器會暴露很多封裝、遍歷和查詢其內部的 Vue 元件例項的便捷的方法。

                          // test.js
                          
                          // 從測試實用工具集中匯入 `mount()` 方法
                          // 同時匯入你要測試的元件
                          import { mount } from '@vue/test-utils'
                          import Counter from './counter'
                          
                          // 現在掛載元件,你便得到了這個包裹器
                          const wrapper = mount(Counter)
                          
                          // 你可以通過 `wrapper.vm` 訪問實際的 Vue 例項
                          const vm = wrapper.vm
                          
                          // 在控制檯將其記錄下來即可深度審閱包裹器
                          // 我們對 Vue Test Utils 的探索也由此開始
                          console.log(wrapper)
                          
                          

                          在掛載的同時,可以設定元件的各種屬性:

                          const wrapper = mount(Counter,{
                              localVue,data() {
                                  return {
                                      bar: 'my-override'
                                  }
                              },propsData: {
                                  msg: 'abc'
                              },parentComponent: Foo,// 指定父元件
                              provide: {
                                  foo() {
                                      return 'fooValue'
                                  }
                              }
                          })
                          

                          測試元件渲染出來的 HTML

                          通過包裹器wrapper的相關方法,判斷元件渲染出來的HTML是否符合預期。

                          import { mount } from '@vue/test-utils'
                          import Counter from './counter'
                          
                          describe('Counter',() => {
                            // 現在掛載元件,你便得到了這個包裹器
                            const wrapper = mount(Counter)
                          
                            test('renders the correct markup',() => {
                              expect(wrapper.html()).toContain('<span class="count">0</span>')
                            })
                          
                            // 也便於檢查已存在的元素
                            test('has a button',() => {
                              expect(wrapper.contains('button')).toBe(true)
                            })
                          })
                          
                          

                          模擬使用者操作

                          當用戶點選按鈕的時候,我們的計數器應該遞增。為了模擬這一行為,我們首先需要通過 wrapper.find() 定位該按鈕,此方法返回一個該按鈕元素的包裹器。然後我們能夠通過對該按鈕包裹器呼叫 .trigger() 來模擬點選。

                          it('button click should increment the count',() => {
                            expect(wrapper.vm.count).toBe(0)
                            const button = wrapper.find('button')
                            button.trigger('click')
                            expect(wrapper.vm.count).toBe(1)
                          })
                          

                          為了測試計數器中的文字是否已經更新,我們需要了解 nextTick。任何導致操作 DOM 的改變都應該在斷言之前 await nextTick 函式。

                          it('button click should increment the count text',async () => {
                            expect(wrapper.text()).toContain('0')
                            const button = wrapper.find('button')
                            await button.trigger('click')
                            expect(wrapper.text()).toContain('1')
                          })
                          

                          元件的事件

                          每個掛載的包裹器都會通過其背後的 Vue 例項自動記錄所有被觸發的事件。你可以用 wrapper.emitted() 方法取回這些事件記錄。

                          wrapper.vm.$emit('foo')
                          wrapper.vm.$emit('foo',123)
                          
                          /*
                          `wrapper.emitted()` 返回以下物件:
                          {
                            foo: [[],[123]]
                          }
                          */
                          
                          

                          然後你可以基於這些資料來設定斷言:

                          // 斷言事件已經被觸發
                          expect(wrapper.emitted().foo).toBeTruthy()
                          
                          // 斷言事件的數量
                          expect(wrapper.emitted().foo.length).toBe(2)
                          
                          // 斷言事件的有效資料
                          expect(wrapper.emitted().foo[1]).toEqual([123])
                          
                          

                          還可以觸發子元件的事件:

                          import { mount } from '@vue/test-utils'
                          import ParentComponent from '@/components/ParentComponent'
                          import ChildComponent from '@/components/ChildComponent'
                          
                          describe('ParentComponent',() => {
                            test("displays 'Emitted!' when custom event is emitted",() => {
                              const wrapper = mount(ParentComponent)
                              wrapper.find(ChildComponent).vm.$emit('custom')
                              expect(wrapper.html()).toContain('Emitted!')
                            })
                          })
                          
                          

                          元件的data

                          可以使用 setData() 或 setProps 設定元件的狀態資料:

                          it('manipulates state',async () => {
                            await wrapper.setData({ count: 10 })
                          
                            await wrapper.setProps({ foo: 'bar' })
                          })
                          
                          

                          模擬vue例項方法

                          由於Vue Test Utils 的 setMethods() 即將廢棄,推薦使用 jest.spyOn() 方法來模擬Vue例項方法:

                          import MyComponent from '@/components/MyComponent.vue'
                          
                          describe('MyComponent',() => {
                            it('click does something',async () => {
                              const mockMethod = jest.spyOn(MyComponent.methods,'doSomething')
                              await shallowMount(MyComponent).find('button').trigger('click')
                              expect(mockMethod).toHaveBeenCalled()
                            })
                          })
                          
                          

                          全域性外掛

                          如果你需要安裝所有 test 都使用的全域性外掛,可以使用 setupFiles,先在 jest.config.js 中指定 setup 檔案:

                          // jest.config.js
                          module.exports = {
                              setupFiles: ['<rootDir>/tests/unit/setup.js']
                          }
                          

                          然後在 setup.js 使用:

                          // setup.js
                          import Vue from 'vue'
                          
                          // 以下全域性註冊的外掛在jest中不生效,必須使用localVue
                          import ElementUI from 'element-ui'
                          import VueClipboard from 'vue-clipboard2'
                          
                          Vue.use(ElementUI)
                          Vue.use(VueClipboard)
                          
                          Vue.config.productionTip = false
                          
                          

                          當你只是想在某些 test 中安裝全域性外掛時,可以使用 localVue,這會建立一個臨時的Vue例項:

                          import { createLocalVue,mount } from '@vue/test-utils'
                          
                          // 建立一個擴充套件的 `Vue` 建構函式
                          const localVue = createLocalVue()
                          
                          // 正常安裝外掛
                          localVue.use(MyPlugin)
                          
                          // 在掛載選項中傳入 `localVue`
                          mount(Component,{
                            localVue
                          })
                          
                          

                          測試watch

                          假如我們有一個這樣的watcher:

                          watch: {
                            inputValue(newVal,oldVal) {
                              if (newVal.trim().length && newVal !== oldVal) {
                                console.log(newVal)
                              }
                            }
                          }
                          

                          由於watch的呼叫是非同步的,並且在下一個tick才會呼叫,因此可以通過檢測watcher裡的方法是否被呼叫來檢測watch是否生效,使用 jest.spyOn() 方法:

                          describe('Form.test.js',() => {
                            let cmp
                            ...
                          
                            describe('Watchers - inputValue',() => {
                              let spy
                          
                              beforeAll(() => {
                                spy = jest.spyOn(console,'log')
                              })
                          
                              afterEach(() => {
                                spy.mockClear()
                              })
                          
                              it('is not called if value is empty (trimmed)',() => {
                              })
                          
                              it('is not called if values are the same',() => {
                              })
                          
                              it('is called with the new value in other cases',() => {
                              })
                            })
                          })
                          
                          it("is called with the new value in other cases",done => {
                            cmp.vm.inputValue = "foo";
                            cmp.vm.$nextTick(() => {
                              expect(spy).toBeCalled();
                              done();
                            });
                          });
                          
                          

                          第三方外掛

                          當我們使用一些第三方外掛的時候,一般不需要關心其內部的實現,不需要測試其元件,可以使用 shallowMount 代替 mount,減少不必要的渲染:

                          import { shallowMount } from '@vue/test-utils'
                          
                          const wrapper = shallowMount(Component)
                          wrapper.vm // 掛載的 Vue 例項
                          
                          還可以通過 findAllComponents 來查詢第三方元件:
                          import { Select } from 'element-ui'
                          test('選中總部時不顯示分部和網點',async () => {
                              await wrapper.setProps({
                                  value: {
                                      clusterType: 'head-quarter-sit',branch: '',site: ''
                                  }
                              })
                              // 總部不顯示分部和網點
                              expect(wrapper.findAllComponents(Select)).toHaveLength(1)
                          })
                          
                          

                          六、總結

                          單元測試理論

                          • 單元測試能夠持續驗證程式碼的正確性、驅動開發,並起到一定的文件作用;
                          • 測試時資料儘量模擬現實,只考慮測試,不考慮內部程式碼;
                          • 測試時充分考慮資料的邊界條件
                          • 對重點、複雜、核心程式碼,重點測試
                          • 編寫單元測試有以下階段:準備階段、執行階段、斷言階段、清理階段;
                          • 單元測試的工具可分為三類:測試執行器(Test Runner)、測試框架、工具庫。

                          Jest

                          • --watch 選項可以監聽檔案的編碼,自動執行單元測試;
                          • 測試非同步程式碼可以用 done 方法或 aync 函式;
                          • mock函式可以捕獲這個函式的呼叫、this、返回值等,測試回撥函式時非常有用。

                          Vue Test Utils

                          • 用 mount 方法掛載元件,並可自定義各種vue屬性;
                          • shallowMount 方法不渲染子元件,從而加快測試速度;
                          • setupFiles 可以設定全域性環境,如安裝 element-ui;
                          • createLocalVue 可在建立單獨的vue例項,與全域性的隔離;

                          到此這篇關於前端Vue單元測試入門教程的文章就介紹到這了,更多相關Vue單元測試內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!