耀极客论坛

 找回密码
 立即注册
查看: 584|回复: 0

前端Vue单元测试入门教程

[复制链接]

336

主题

318

帖子

22万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
220555
发表于 2022-5-9 01:14:10 | 显示全部楼层 |阅读模式
  单元测试是用来测试项目中的一个模块的功能,本文主要介绍了前端Vue单元测试入门教程,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

一、为什么需要单元测试

  单元测试是用来测试项目中的一个模块的功能,如函数、类、组件等。单元测试的作用有以下:
       
  • 正确性:可以验证代码的正确性,为上线前做更详细的准备;   
  • 自动化:测试用例可以整合到代码版本管理中,自动执行单元测试,避免每次手工操作;   
  • 解释性:能够为其他开发人员提供被测模块的文档参考,阅读测试用例可能比文档更完善;   
  • 驱动开发、指导设计:提前写好的单元测试能够指导开发的API设计,也能够提前发现设计中的问题;   
  • 保证重构:测试用例可以多次验证,当需要回归测试时能够节省大量时间。

二、如何写单元测试

  测试原则
       
  • 测试代码时,只考虑测试,不考虑内部实现   
  • 数据尽量模拟现实,越靠近现实越好   
  • 充分考虑数据的边界条件   
  • 对重点、复杂、核心代码,重点测试   
  • 测试、功能开发相结合,有利于设计和代码重构
  编写步骤
       
  • 准备阶段:构造参数,创建 spy 等   
  • 执行阶段:用构造好的参数执行被测试代码   
  • 断言阶段:用实际得到的结果与期望的结果比较,以判断该测试是否正常   
  • 清理阶段:清理准备阶段对外部环境的影响,移除在准备阶段创建的 spy 等

三、测试工具

  单元测试的工具可分为三类:
       
  • 测试运行器(Test Runner):可以模拟各种浏览器环境,自定义配置测试框架和断言库等,如Karma.   
  • 测试框架:提供单元测试的功能模块,常见的框架有Jest, mocha, Jasmine, QUnit.   
  • 工具库:assert, should.js, expect.js, chai.js等断言库,enzyme渲染库,Istanbul覆盖率计算。
  这里,我们将使用 Jest 作为例子。Jest 功能全面,集成了各种工具,且配置简单,甚至零配置直接使用。


四、Jest入门

  Jest 官网的描述是这样的:
  1. Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
复制代码
安装
  1. yarn add --dev jest
  2. # or
  3. # npm install -D jest
复制代码
简单示例

  从官网提供的示例开始,测试一个函数,这个函数完成两个数字的相加,创建一个 sum.js 文件︰
  1. function sum(a, b) {
  2.   return a + b;
  3. }
  4. module.exports = sum;
复制代码
  然后,创建 sum.test.js 文件︰
  1. const sum = require('./sum');
  2. test('adds 1 + 2 to equal 3', () => {
  3.   expect(sum(1, 2)).toBe(3);
  4. });
  5. package.json 里增加一个测试任务:
  6. {
  7.   "scripts": {
  8.     "test": "jest"
  9.   }
  10. }
复制代码
  最后,运行 yarn test 或 npm run test ,Jest将打印下面这个消息:
  1. PASS  ./sum.test.js
  2. ✓ 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) ,并为其指定各种有用的配置项。如:
  1. jest my-test --notify --config=config.json
复制代码
  Jest 命令有以下常见参数:
       
  • --coverage 表示输出单元测试覆盖率,覆盖率文件默认在 tests/unit/coverage/lcov-report/index.html;   
  • --watch 监听模式,与测试用例相关的文件更改时都会重新触发单元测试。
  更多选项查看Jest CLI Options.


使用配置文件

  使用 jest 命令可生成一个配置文件:
  1. jest --init
复制代码
  过程中会有几个选项供你选择:
  1. √ Would you like to use Typescript for the configuration file? ... no
  2. √ Choose the test environment that will be used for testing » jsdom (browser-like)
  3. √ Do you want Jest to add coverage reports? ... yes
  4. √ Which provider should be used to instrument code for coverage? » babel
  5. √ Automatically clear mock calls and instances between every test? ... yes
复制代码
  配置文件示例(不是基于上述选择):
  1. // jest.config.js
  2. const path = require('path')
  3. module.exports = {
  4.     preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
  5.     rootDir: path.resolve(__dirname, './'),
  6.     coverageDirectory: '‹rootDir>/tests/unit/coverage',
  7.     collectCoverageFrom: [
  8.         'src/*.{js,ts,vue}',
  9.         'src/directives/*.{js,ts,vue}',
  10.         'src/filters/*.{js,ts,vue}',
  11.         'src/helper/*.{js,ts,vue}',
  12.         'src/views/**/*.{js,ts,vue}',
  13.         'src/services/*.{js,ts,vue}'
  14.     ]
  15. }
复制代码
使用 Babel
  1. yarn add --dev babel-jest @babel/core @babel/preset-env
复制代码
  可以在工程的根目录下创建一个babel.config.js文件用于配置与你当前Node版本兼容的Babel:
  1. // babel.config.js
  2. module.exports = {
  3.   presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
  4. };
复制代码
vue-cli 中使用 Jest

  在项目中安装 @vue/cli-plugin-unit-jest 插件,即可在 vue-cli 中使用 Jest:
  1. vue add unit-jest
  2. # or
  3. # yarn add -D @vue/cli-plugin-unit-jest @types/jest
复制代码
  1. "scripts": {
  2.     "test:unit": "vue-cli-service test:unit --coverage"
  3. },
复制代码
  @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() 检查两个基本类型是否精确匹配:
  1. test('two plus two is four', () => {
  2.   expect(2 + 2).toBe(4);
  3. });
复制代码
  toEqual() 检查对象是否相等:
  1. test('object assignment', () => {
  2.   const data = {one: 1};
  3.   data['two'] = 2;
  4.   expect(data).toEqual({one: 1, two: 2});
  5. });
复制代码
检查类false值
       
  • toBeNull 只匹配 null   
  • toBeUndefined 只匹配 undefined   
  • toBeDefined 与 toBeUndefined 相反   
  • toBeTruthy 匹配任何 if 语句为真   
  • toBeFalsy 匹配任何 if 语句为假
  示例:
  1. test('null', () => {
  2.   const n = null;
  3.   expect(n).toBeNull();
  4.   expect(n).toBeDefined();
  5.   expect(n).not.toBeUndefined();
  6.   expect(n).not.toBeTruthy();
  7.   expect(n).toBeFalsy();
  8. });
  9. test('zero', () => {
  10.   const z = 0;
  11.   expect(z).not.toBeNull();
  12.   expect(z).toBeDefined();
  13.   expect(z).not.toBeUndefined();
  14.   expect(z).not.toBeTruthy();
  15.   expect(z).toBeFalsy();
  16. });
复制代码
数字大小比较
  1. test('two plus two', () => {
  2.   const value = 2 + 2;
  3.   expect(value).toBeGreaterThan(3);
  4.   expect(value).toBeGreaterThanOrEqual(3.5);
  5.   expect(value).toBeLessThan(5);
  6.   expect(value).toBeLessThanOrEqual(4.5);
  7.   // toBe and toEqual are equivalent for numbers
  8.   expect(value).toBe(4);
  9.   expect(value).toEqual(4);
  10. });
复制代码
  对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。
  1. test('两个浮点数字相加', () => {
  2.   const value = 0.1 + 0.2;
  3.   //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  4.   expect(value).toBeCloseTo(0.3); // 这句可以运行
  5. });
复制代码
字符串比较

  可以使用正则表达式检查:
  1. test('there is no I in team', () => {
  2.   expect('team').not.toMatch(/I/);
  3. });
  4. test('but there is a "stop" in Christoph', () => {
  5.   expect('Christoph').toMatch(/stop/);
  6. });
复制代码
数组和类数组

  你可以通过 toContain 来检查一个数组或可迭代对象是否包含某个特定项:
  1. const shoppingList = [
  2.   'diapers',
  3.   'kleenex',
  4.   'trash bags',
  5.   'paper towels',
  6.   'milk',
  7. ];
  8. test('the shopping list has milk on it', () => {
  9.   expect(shoppingList).toContain('milk');
  10.   expect(new Set(shoppingList)).toContain('milk');
  11. });
复制代码
异常

  还可以用来检查一个函数是否抛出异常:
  1. function compileAndroidCode() {
  2.   throw new Error('you are using the wrong JDK');
  3. }
  4. test('compiling android goes as expected', () => {
  5.   expect(() => compileAndroidCode()).toThrow();
  6.   expect(() => compileAndroidCode()).toThrow(Error);
  7.   // You can also use the exact error message or a regexp
  8.   expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  9.   expect(() => compileAndroidCode()).toThrow(/JDK/);
  10. });
复制代码
  更多使用方法参考API文档.

只执行当前test

  可使用 only() 方法表示只执行这个test,减少不必要的重复测试:
  1. test.only('it is raining', () => {
  2.   expect(inchesOfRain()).toBeGreaterThan(0);
  3. });
  4. test('it is not snowing', () => {
  5.   expect(inchesOfSnow()).toBe(0);
  6. });
复制代码
测试异步代码


回调函数

  例如,假设您有一个 fetchData(callback) 函数,获取一些数据并在完成时调用 callback(data)。 你期望返回的数据是一个字符串 'peanut butter':
  1. test('the data is peanut butter', done => {
  2.   function callback(data) {
  3.     try {
  4.       expect(data).toBe('peanut butter');
  5.       done();
  6.     } catch (error) {
  7.       done(error);
  8.     }
  9.   }
  10.   fetchData(callback);
  11. });
复制代码
  使用 done() 是为了标识这个 test 执行完毕,如果没有这个 done(),在 test 执行完毕后,我们的单元测试就结束了,这不符合我们的预期,因为callback还未调用,单元测试还没走完。若 done() 函数从未被调用,将会提示超时错误。

  若 expect 执行失败,它会抛出一个错误,后面的 done() 不再执行。 若我们想知道测试用例为何失败,我们必须将 expect 放入 try 中,将 error 传递给 catch 中的 done 函数。 否则,最后控制台将显示一个超时错误失败,不能显示我们在 expect(data) 中接收的值。


Promises

  还是使用上面的例子:
  1. test('the data is peanut butter', () => {
  2.   return fetchData().then(data => {
  3.     expect(data).toBe('peanut butter');
  4.   });
  5. });
复制代码
  一定不要忘记 return 结果,这样才能确保测试和功能同时结束。
如果是期望 Promise 被 reject, 则使用 catch 方法:
  1. test('the fetch fails with an error', () => {
  2.   expect.assertions(1);
  3.   return fetchData().catch(e => expect(e).toMatch('error'));
  4. });
复制代码
  还可以使用 resolves 和 rejects 匹配器:
  1. test('the data is peanut butter', () => {
  2.   return expect(fetchData()).resolves.toBe('peanut butter');
  3. });
  4. test('the fetch fails with an error', () => {
  5.   return expect(fetchData()).rejects.toMatch('error');
  6. });
复制代码
Async/Await
  1. test('the data is peanut butter', async () => {
  2.   const data = await fetchData();
  3.   expect(data).toBe('peanut butter');
  4. });
  5. test('the fetch fails with an error', async () => {
  6.   expect.assertions(1);
  7.   try {
  8.     await fetchData();
  9.   } catch (e) {
  10.     expect(e).toMatch('error');
  11.   }
  12. });
复制代码
  async/await 还可以和 resolves()/rejects() 结合使用:
  1. test('the data is peanut butter', async () => {
  2.   await expect(fetchData()).resolves.toBe('peanut butter');
  3. });
  4. test('the fetch fails with an error', async () => {
  5.   await expect(fetchData()).rejects.toMatch('error');
  6. });
复制代码
安装和拆卸


测试前和测试后

  在某些情况下,我们开始测试前需要做一些准备工作,然后在测试完成后,要做一些清理工作,可以使用 beforeEach 和 afterEach。
例如,我们在每个test前需要初始化一些城市数据,test结束后要清理掉:
  1. beforeEach(() => {
  2.   initializeCityDatabase();
  3. });
  4. afterEach(() => {
  5.   clearCityDatabase();
  6. });
  7. test('city database has Vienna', () => {
  8.   expect(isCity('Vienna')).toBeTruthy();
  9. });
  10. test('city database has San Juan', () => {
  11.   expect(isCity('San Juan')).toBeTruthy();
  12. });
复制代码
  类似的还有 beforeAll 和 afterAll,在当前spec测试文件开始前和结束后的单次执行。


测试用例分组

  默认情况下,before 和 after 的块可以应用到文件中的每个测试。 此外可以通过 describe 块来将测试分组。 当 before 和 after 的块在 describe 块内部时,则其只适用于该 describe 块内的测试。
  1. // Applies to all tests in this file
  2. beforeEach(() => {
  3.   return initializeCityDatabase();
  4. });
  5. test('city database has Vienna', () => {
  6.   expect(isCity('Vienna')).toBeTruthy();
  7. });
  8. test('city database has San Juan', () => {
  9.   expect(isCity('San Juan')).toBeTruthy();
  10. });
  11. describe('matching cities to foods', () => {
  12.   // Applies only to tests in this describe block
  13.   beforeEach(() => {
  14.     return initializeFoodDatabase();
  15.   });
  16.   test('Vienna ‹3 sausage', () => {
  17.     expect(isValidCityFoodPair('Vienna', 'Wiener Würstchen')).toBe(true);
  18.   });
  19.   test('San Juan ‹3 plantains', () => {
  20.     expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
  21.   });
  22. });
复制代码
执行顺序

  由于使用了 describe 进行分组,于是就有了嵌套的作用域,各生命周期的执行顺序如下:
       
  • 外层作用域的 before 比内层的先执行,而 after 则相反;   
  • 同一层级 beforeAll 比 beforeEach 先执行,after 则相反;
  1. beforeAll(() => console.log('1 - beforeAll'));
  2. afterAll(() => console.log('1 - afterAll'));
  3. beforeEach(() => console.log('1 - beforeEach'));
  4. afterEach(() => console.log('1 - afterEach'));
  5. test('', () => console.log('1 - test'));
  6. describe('Scoped / Nested block', () => {
  7.   beforeAll(() => console.log('2 - beforeAll'));
  8.   afterAll(() => console.log('2 - afterAll'));
  9.   beforeEach(() => console.log('2 - beforeEach'));
  10.   afterEach(() => console.log('2 - afterEach'));
  11.   test('', () => console.log('2 - test'));
  12. });
  13. // 1 - beforeAll
  14. // 1 - beforeEach
  15. // 1 - test
  16. // 1 - afterEach
  17. // 2 - beforeAll
  18. // 1 - beforeEach
  19. // 2 - beforeEach
  20. // 2 - test
  21. // 2 - afterEach
  22. // 1 - afterEach
  23. // 2 - afterAll
  24. // 1 - afterAll
复制代码
mock 函数

  jest.fn() 可以用来生成一个 mock 函数,jest 可以捕获这个函数的调用、this、返回值等,这在测试回调函数时非常有用。


测试mock

  假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。
  1. function forEach(items, callback) {
  2.   for (let index = 0; index ‹ items.length; index++) {
  3.     callback(items[index]);
  4.   }
  5. }
复制代码
  为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。
  1. const mockCallback = jest.fn(x => 42 + x);
  2. forEach([0, 1], mockCallback);
  3. // 此 mock 函数被调用了两次
  4. expect(mockCallback.mock.calls.length).toBe(2);
  5. // 第一次调用函数时的第一个参数是 0
  6. expect(mockCallback.mock.calls[0][0]).toBe(0);
  7. // 第二次调用函数时的第一个参数是 1
  8. expect(mockCallback.mock.calls[1][0]).toBe(1);
  9. // 第一次函数调用的返回值是 42
  10. expect(mockCallback.mock.results[0].value).toBe(42);
复制代码
mock的返回值

  Mock 函数也可以用于在测试期间将测试值注入代码︰
  1. const myMock = jest.fn();
  2. console.log(myMock());
  3. // > undefined
  4. myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
  5. console.log(myMock(), myMock(), myMock(), myMock());
  6. // > 10, 'x', true, true
复制代码
模拟接口返回

  假定有个从 API 获取用户的类。 该类用 axios 调用 API 然后返回 data,其中包含所有用户的属性:
  1. // users.js
  2. import axios from 'axios';
  3. class Users {
  4.   static all() {
  5.     return axios.get('/users.json').then(resp => resp.data);
  6.   }
  7. }
  8. export default Users;
复制代码
  现在,为测试该方法而不实际调用 API (使测试缓慢与脆弱),我们可以用 jest.mock(...) 函数自动模拟 axios 模块。一旦模拟模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。
  1. // users.test.js
  2. import axios from 'axios';
  3. import Users from './users';
  4. jest.mock('axios');
  5. test('should fetch users', () => {
  6.   const users = [{name: 'Bob'}];
  7.   const resp = {data: users};
  8.   axios.get.mockResolvedValue(resp);
  9.   // or you could use the following depending on your use case:
  10.   // axios.get.mockImplementation(() => Promise.resolve(resp))
  11.   return Users.all().then(data => expect(data).toEqual(users));
  12. });
复制代码
mock函数的匹配器

  有了mock功能,就可以给函数增加一些自定义匹配器:
  1. // The mock function was called at least once
  2. expect(mockFunc).toHaveBeenCalled();
  3. // The mock function was called at least once with the specified args
  4. expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
  5. // The last call to the mock function was called with the specified args
  6. expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
  7. // All calls and the name of the mock is written as a snapshot
  8. expect(mockFunc).toMatchSnapshot();
  9. 也可以自己通过原生的匹配器模拟,下方的代码与上方的等价:
  10. // The mock function was called at least once
  11. expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
  12. // The mock function was called at least once with the specified args
  13. expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
  14. // The last call to the mock function was called with the specified args
  15. expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  16.   arg1,
  17.   arg2,
  18. ]);
  19. // The first arg of the last call to the mock function was `42`
  20. // (note that there is no sugar helper for this specific of an assertion)
  21. expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
  22. // A snapshot will check that a mock was invoked the same number of times,
  23. // in the same order, with the same arguments.
  24. expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
  25. expect(mockFunc.getMockName()).toBe('a mock name');
复制代码
五、Vue Test Utils

  官网是这样介绍 Vue Test Utils 的:
  1. Vue Test Utils 是 Vue.js 官方的单元测试实用工具库。
复制代码
  以下的例子均基于 vue-cli 脚手架,包括 webpack/babel/vue-loader


测试单文件组件

  Vue 的单文件组件在它们运行于 Node 或浏览器之前是需要预编译的。我们推荐两种方式完成编译:通过一个 Jest 预编译器,或直接使用 webpack。这里我们选用 Jest 的方式。
  1. yarn add -D jest @vue/test-utils vue-jest
复制代码
  vue-jest 目前并不支持 vue-loader 所有的功能,比如自定义块和样式加载。额外的,诸如代码分隔等 webpack 特有的功能也是不支持的。如果要使用这些不支持的特性,你需要用 Mocha 取代 Jest 来运行你的测试,同时用 webpack 来编译你的组件。

处理 webpack 别名

  vue-cli 中默认使用 @ 作为 /src 的别名,在 Jest 也需要单独配置:
  1. // jest.config.js
  2. module.exports = {
  3.     moduleNameMapper: {
  4.         '^@/(.*)$': '‹rootDir>/src/$1'
  5.     }
  6. }
复制代码
挂载组件

  被挂载的组件会返回到一个包裹器内,而包裹器会暴露很多封装、遍历和查询其内部的 Vue 组件实例的便捷的方法。
  1. // test.js
  2. // 从测试实用工具集中导入 `mount()` 方法
  3. // 同时导入你要测试的组件
  4. import { mount } from '@vue/test-utils'
  5. import Counter from './counter'
  6. // 现在挂载组件,你便得到了这个包裹器
  7. const wrapper = mount(Counter)
  8. // 你可以通过 `wrapper.vm` 访问实际的 Vue 实例
  9. const vm = wrapper.vm
  10. // 在控制台将其记录下来即可深度审阅包裹器
  11. // 我们对 Vue Test Utils 的探索也由此开始
  12. console.log(wrapper)
复制代码
  在挂载的同时,可以设置组件的各种属性:
  1. const wrapper = mount(Counter, {
  2.     localVue,
  3.     data() {
  4.         return {
  5.             bar: 'my-override'
  6.         }
  7.     },
  8.     propsData: {
  9.         msg: 'abc'
  10.     },
  11.     parentComponent: Foo, // 指定父组件
  12.     provide: {
  13.         foo() {
  14.             return 'fooValue'
  15.         }
  16.     }
  17. })
复制代码
测试组件渲染出来的 HTML

  通过包裹器wrapper的相关方法,判断组件渲染出来的HTML是否符合预期。
  1. import { mount } from '@vue/test-utils'
  2. import Counter from './counter'
  3. describe('Counter', () => {
  4.   // 现在挂载组件,你便得到了这个包裹器
  5.   const wrapper = mount(Counter)
  6.   test('renders the correct markup', () => {
  7.     expect(wrapper.html()).toContain('‹span class="count">0‹/span>')
  8.   })
  9.   // 也便于检查已存在的元素
  10.   test('has a button', () => {
  11.     expect(wrapper.contains('button')).toBe(true)
  12.   })
  13. })
复制代码
模拟用户操作

  当用户点击按钮的时候,我们的计数器应该递增。为了模拟这一行为,我们首先需要通过 wrapper.find() 定位该按钮,此方法返回一个该按钮元素的包裹器。然后我们能够通过对该按钮包裹器调用 .trigger() 来模拟点击。
  1. it('button click should increment the count', () => {
  2.   expect(wrapper.vm.count).toBe(0)
  3.   const button = wrapper.find('button')
  4.   button.trigger('click')
  5.   expect(wrapper.vm.count).toBe(1)
  6. })
复制代码
  为了测试计数器中的文本是否已经更新,我们需要了解 nextTick。任何导致操作 DOM 的改变都应该在断言之前 await nextTick 函数。
  1. it('button click should increment the count text', async () => {
  2.   expect(wrapper.text()).toContain('0')
  3.   const button = wrapper.find('button')
  4.   await button.trigger('click')
  5.   expect(wrapper.text()).toContain('1')
  6. })
复制代码
组件的事件

  每个挂载的包裹器都会通过其背后的 Vue 实例自动记录所有被触发的事件。你可以用 wrapper.emitted() 方法取回这些事件记录。
  1. wrapper.vm.$emit('foo')
  2. wrapper.vm.$emit('foo', 123)
  3. /*
  4. `wrapper.emitted()` 返回以下对象:
  5. {
  6.   foo: [[], [123]]
  7. }
  8. */
复制代码
  然后你可以基于这些数据来设置断言:
  1. // 断言事件已经被触发
  2. expect(wrapper.emitted().foo).toBeTruthy()
  3. // 断言事件的数量
  4. expect(wrapper.emitted().foo.length).toBe(2)
  5. // 断言事件的有效数据
  6. expect(wrapper.emitted().foo[1]).toEqual([123])
复制代码
  还可以触发子组件的事件:
  1. import { mount } from '@vue/test-utils'
  2. import ParentComponent from '@/components/ParentComponent'
  3. import ChildComponent from '@/components/ChildComponent'
  4. describe('ParentComponent', () => {
  5.   test("displays 'Emitted!' when custom event is emitted", () => {
  6.     const wrapper = mount(ParentComponent)
  7.     wrapper.find(ChildComponent).vm.$emit('custom')
  8.     expect(wrapper.html()).toContain('Emitted!')
  9.   })
  10. })
复制代码
组件的data

  可以使用 setData() 或 setProps 设置组件的状态数据:
  1. it('manipulates state', async () => {
  2.   await wrapper.setData({ count: 10 })
  3.   await wrapper.setProps({ foo: 'bar' })
  4. })
复制代码
模拟vue实例方法

  由于Vue Test Utils 的 setMethods() 即将废弃,推荐使用 jest.spyOn() 方法来模拟Vue实例方法:
  1. import MyComponent from '@/components/MyComponent.vue'
  2. describe('MyComponent', () => {
  3.   it('click does something', async () => {
  4.     const mockMethod = jest.spyOn(MyComponent.methods, 'doSomething')
  5.     await shallowMount(MyComponent).find('button').trigger('click')
  6.     expect(mockMethod).toHaveBeenCalled()
  7.   })
  8. })
复制代码
全局插件

  如果你需要安装所有 test 都使用的全局插件,可以使用 setupFiles,先在 jest.config.js 中指定 setup 文件:
  1. // jest.config.js
  2. module.exports = {
  3.     setupFiles: ['‹rootDir>/tests/unit/setup.js']
  4. }
复制代码
  然后在 setup.js 使用:
  1. // setup.js
  2. import Vue from 'vue'
  3. // 以下全局注册的插件在jest中不生效,必须使用localVue
  4. import ElementUI from 'element-ui'
  5. import VueClipboard from 'vue-clipboard2'
  6. Vue.use(ElementUI)
  7. Vue.use(VueClipboard)
  8. Vue.config.productionTip = false
复制代码
  当你只是想在某些 test 中安装全局插件时,可以使用 localVue,这会创建一个临时的Vue实例:
  1. import { createLocalVue, mount } from '@vue/test-utils'
  2. // 创建一个扩展的 `Vue` 构造函数
  3. const localVue = createLocalVue()
  4. // 正常安装插件
  5. localVue.use(MyPlugin)
  6. // 在挂载选项中传入 `localVue`
  7. mount(Component, {
  8.   localVue
  9. })
复制代码
测试watch

  假如我们有一个这样的watcher:
  1. watch: {
  2.   inputValue(newVal, oldVal) {
  3.     if (newVal.trim().length && newVal !== oldVal) {
  4.       console.log(newVal)
  5.     }
  6.   }
  7. }
复制代码
  由于watch的调用是异步的,并且在下一个tick才会调用,因此可以通过检测watcher里的方法是否被调用来检测watch是否生效,使用 jest.spyOn() 方法:
  1. describe('Form.test.js', () => {
  2.   let cmp
  3.   ...
  4.   describe('Watchers - inputValue', () => {
  5.     let spy
  6.     beforeAll(() => {
  7.       spy = jest.spyOn(console, 'log')
  8.     })
  9.     afterEach(() => {
  10.       spy.mockClear()
  11.     })
  12.     it('is not called if value is empty (trimmed)', () => {
  13.     })
  14.     it('is not called if values are the same', () => {
  15.     })
  16.     it('is called with the new value in other cases', () => {
  17.     })
  18.   })
  19. })
  20. it("is called with the new value in other cases", done => {
  21.   cmp.vm.inputValue = "foo";
  22.   cmp.vm.$nextTick(() => {
  23.     expect(spy).toBeCalled();
  24.     done();
  25.   });
  26. });
复制代码
第三方插件

  当我们使用一些第三方插件的时候,一般不需要关心其内部的实现,不需要测试其组件,可以使用 shallowMount 代替 mount, 减少不必要的渲染:
  1. import { shallowMount } from '@vue/test-utils'
  2. const wrapper = shallowMount(Component)
  3. wrapper.vm // 挂载的 Vue 实例
  4. 还可以通过 findAllComponents 来查找第三方组件:
  5. import { Select } from 'element-ui'
  6. test('选中总部时不显示分部和网点', async () => {
  7.     await wrapper.setProps({
  8.         value: {
  9.             clusterType: 'head-quarter-sit',
  10.             branch: '',
  11.             site: ''
  12.         }
  13.     })
  14.     // 总部不显示分部和网点
  15.     expect(wrapper.findAllComponents(Select)).toHaveLength(1)
  16. })
复制代码
六、总结

  单元测试理论
       
  • 单元测试能够持续验证代码的正确性、驱动开发,并起到一定的文档作用;   
  • 测试时数据尽量模拟现实,只考虑测试,不考虑内部代码;   
  • 测试时充分考虑数据的边界条件   
  • 对重点、复杂、核心代码,重点测试   
  • 编写单元测试有以下阶段:准备阶段、执行阶段、断言阶段、清理阶段;   
  • 单元测试的工具可分为三类:测试运行器(Test Runner)、测试框架、工具库。
  Jest
       
  • --watch 选项可以监听文件的编码,自动执行单元测试;   
  • 测试异步代码可以用 done 方法或 aync 函数;   
  • mock函数可以捕获这个函数的调用、this、返回值等,测试回调函数时非常有用。
  Vue Test Utils
       
  • 用 mount 方法挂载组件,并可自定义各种vue属性;   
  • shallowMount 方法不渲染子组件,从而加快测试速度;   
  • setupFiles 可以设置全局环境,如安装 element-ui;   
  • createLocalVue 可在创建单独的vue实例,与全局的隔离;
  到此这篇关于前端Vue单元测试入门教程的文章就介绍到这了,更多相关Vue单元测试内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|耀极客论坛 ( 粤ICP备2022052845号-2 )|网站地图

GMT+8, 2022-12-10 04:23 , Processed in 0.068470 second(s), 20 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表