[wip]Vue3+Jest 单元测试 - 图1

shadow of person’s hand holding flowers - Photo by Tanya Trofymchuk on Unsplash(https://unsplash.com/photos/gzXhH-RiydU)

0 按

本文是 《Vue自动化测试:导读》的后续篇章,一些基础概念会被忽略。

本文把握的核心:

  • 配合 jest + @vue/test-utils@2 + vue3 ,完成技术的选型
  • 通过 describe+it 完成对测试文件的封装

    1 安装

  • 新创建项目,可以在 unit-test 中选择 jest

  • 已创建好的项目,可以通过 vue add unit-jest 来一键完成引入
  • 手动引入,需要考虑下列依赖,这些不是很重要,我就隐藏了,想看自己拖动看。 ```bash

    相关依赖

    npm i @vue/test-utils@next vue-jest@next ts-jest typescript -D

    截至 2021-06-20 @vue/test-utils 是 2.0.0-rc.6

    当前 vue-jest@5

具体的 preset 可以自行查看https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-unit-jest/README.md

jest初始化可以参考:

npx jest —init

  1. <a name="5RpiF"></a>
  2. ## Jest
  3. 之前jest不支持 esm,所以需要babel来转。
  4. <a name="9mBrh"></a>
  5. # 2 Jest 使用
  6. 一个简单的demo:
  7. ```javascript
  8. test('demo',() => {
  9. expect(1+1).toBe(2);
  10. })

jest最佳实践:

  1. 合理使用层级,但不需要特别深
    1. describe > it > test ,实际中不用分到 test
    2. describe 对应 suites
    3. it 对应 tests
  2. 合理使用js判断,减少断言库api的使用,降低认知成本
    1. 看下面 实例 2-1
    2. 断言库常用的就几种,看 实例2-2 和思维导图
  3. 遇到有关timer异步任务
    1. 尽可能只使用 useFakeTimers 配合 useAllTimer ,见 实例2-3
  4. 遇到异步Promise情况,比如fetch:
    1. 一律使用 async/await
    2. 网络请求一律走mock,接口测试独立运行,见 实例2-4

下面是实例2-1,减少对api的依赖:

  1. expect(1+1).toBe(2)
  2. // 可优化成,一个判断是否相等
  3. expect(1+1===2).toBe(true)
  4. expect(2).toBeGreaterThan(1)
  5. // 可优化成js判断
  6. expect(2>1).toBe(true)

下面是实例2-2,常用的对比方法:

  1. expect(.1+.2).toBeCloseTo(0.3,5)
  2. expect([1,2]).toEqual([1,2]) // 对象类型使用 equal
  3. expect({a:{b:1}}).toEqual({a:{b:1}})
  4. expect({a:undefined,b:2}).toEqual({b:2}) // 这里注意
  5. expect({a:unefined,b:2}).not.toStrictEqual({b:2})// 严格相等

下面是常用api一览,不全够用: [wip]Vue3+Jest 单元测试 - 图2 下面是实例2-3,timer不等待:

  1. let i = 0;
  2. export const fnSetTimeout = (fn) => {
  3. setTimeout(() => {
  4. fn({ name: "otto" });
  5. i = 1;
  6. }, 1000);
  7. };
  8. jest.useFakeTimers();
  9. it("测试异步函数", () => {
  10. const fn = jest.fn();
  11. fnSetTimeout(fn);
  12. jest.runAllTimers();
  13. expect(i === 1).toBe(true);
  14. });

下面是实例2-4,mock异步请求

  1. // api.js
  2. import axios from "axios";
  3. export const fetchUser = ()=>{
  4. return axios.get('/user')
  5. }
  6. // 测试这个函数会发出真正的请求,不必要

解决方法:

  • 创建同级目录 __mocks__/api.js ,文件同名,填充对应的内容:
    1. export const fetchUser = ()=>{
    2. return new Promise.resolve({user:'otto'}))
    3. }
    4. // 这个方法和真实fetch形式一致,注意返回体可能需要包裹。
    有了mock文件,这样就简单了,记住接口测试是独立的一个测试方向。 ```javascript jest.mock(‘./api.js’); // 会自动使用同级目录mocks/api.js

import {fetchList,} from ‘./api’; // 引入mock的方法 it(‘fetchUser测试’,async ()=>{ let data = await fetchUser(); expect(data).toEqual({user:’otto’}) })

  1. 注意,直接mock文件有时候不如重写 axios.get ,比如考虑
  2. ```javascript
  3. // __mocks__/axios.js
  4. export default {
  5. get(url){
  6. return new Promise((resolve,reject)=>{
  7. if(url === '/user'){
  8. resolve({user:'otto'});
  9. }
  10. })
  11. }
  12. }

也是一种思路。以上实例2-4结束。

jest config

类似这样,具体可通过查询 preset 来得到:

  1. module.exports = {
  2. moduleFileExtensions: [ // 测试的文件类型
  3. 'js','jsx','json','vue'
  4. ],
  5. transform: { // 转化方式
  6. '^.+\\.vue$': 'vue-jest', // 如果是vue文件使用vue-jest解析
  7. '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', // 如果是图片样式则使用 jest-transform-stub
  8. '^.+\\.jsx?$': 'babel-jest' // 如果是jsx文件使用 babel-jest
  9. },
  10. transformIgnorePatterns: [ // 转化时忽略 node_modules
  11. '/node_modules/'
  12. ],
  13. moduleNameMapper: { // @符号 表示当前项目下的src
  14. '^@/(.*)$': '<rootDir>/src/$1'
  15. },
  16. snapshotSerializers: [ // 快照的配置
  17. 'jest-serializer-vue'
  18. ],
  19. testMatch: [ // 默认测试 /test/unit中包含.spec的文件 和__tests__目录下的文件
  20. '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  21. ],
  22. testURL: 'http://localhost/', // 测试地址
  23. watchPlugins: [ // watch提示插件
  24. 'jest-watch-typeahead/filename',
  25. 'jest-watch-typeahead/testname'
  26. ]
  27. }

3 vue-test-utils

vtu 是 vue-test-utils 的简称,有时候在vue内部会这样称呼。

因为 vue3 单文件是 vue后缀,不是普通的js文件,所以需要使用一个插件来完成转化。把转化后的结果交给jest做进一步判断,这个插件就是官方的 vue-test-utils

vue3 的 vue-test-utils 文档 https://next.vue-test-utils.vuejs.org/

最佳实践:

  • 鼓励moun通过 mount(vue组件) 得到实例,可配合 stub 或者 干脆 shallMount

基本api

针对具体的实例:

  • setValue 给input设定值
  • trigger 触发某个动作
  • get(string) 查找dom
  • findAll(string) 查找所有dom 后面跟断言 haveLength(2)
  • classes() 获得class name 后面跟断言 .toContain(string) 接字符串
  • findComponent 嵌套的自定义组件
    • .exists() 是否存在 .toBeTruthy()

最佳实践,为了找到某个确定的元素,建议开发时候有意识添加 data-test='xxx'

注意,vue是异步更新dom,一系列的操作会 nexttick 更新,所以 async/await 等待某个操作结束

router vuex 等

有些组件,比如 router-link route-view ,在渲染时候会提示找不到对应组件,因为这是在 app初始化时候挂载的,同理 element-plus 也是。

两种种解决思路:

  • 挂载
  • 模拟

挂载。就是玩真的,导入对应组件,真的去使用插件。 createLocalVue 这个后面具体说。
模拟。这个词叫 存根stubs ,很奇怪的称呼,作用是忽略指定的组件

假定存在这个组件

  1. <template>
  2. <div>
  3. <h1>当前路由:{{this.$route.path}}</h1>
  4. <router-link to="/">首页</router-link>
  5. <router-link to="/about">关于页面</router-link>
  6. <router-view></router-view>
  7. </div>
  8. </template>

槽点满满:

  • 哪来的 $router,这么一看还是 vue3 的 useRouter 好一些,来源真实
  • 哪来的 link view
  1. it("测试Nav组件", () => {
  2. let wrapper = shallowMount(Nav,{
  3. // 忽略这两个组件
  4. stubs:['router-link','router-view'],
  5. mocks:{ // mock一些数据传入到Nav组件中
  6. $route:{path:'/'}
  7. }
  8. });
  9. expect(wrapper.find('h1').text()).toContain('/')
  10. });

妥了,忽略两个全局组件,模拟行为。


实际中我会修改vue提供的 jest.config.js

  1. module.exports = {
  2. preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel",
  3. moduleFileExtensions: ["vue", "ts", "d.ts", "js", "tsx", "json"],
  4. testPathIgnorePatterns: ["/node_modules/", "**/node_modules/**/*"],
  5. };

参考 第十章, p113附近