在vue项目中使用单元测试

更新日期: 2021-06-22阅读: 1.5k标签: 测试

目前笔者负责的项目迭代十分频繁,前端需要处理业务逻辑日益增多,除了考虑如何保证开发速度之外,还必须考虑系统的稳定性,因此决定在项目引入单元测试。

本文主要整理在vue项目中如何使用单元测试,包括如何编写测试用例、如何编写易于测试的代码等问题。

    1. 安装环境

    由于项目是使用vue-cli搭建的,因此添加vue-test-util套件还是比较容易的,主要是添加jest及相关依赖

    vue add unit-jest

    然后会自动下载相关依赖,同时生成一个tests目录,此外还会自动生成一个示例测试文件

    接着运行单元测试的命令(这个命令也是在上一步中自动添加到package.json中的)

    npm run test:unit

    没报错的话就说明环境应该搭好了。

    当然,一个真实的项目不会像HelloWorl这样简单,下面整理实际项目中遇见的一些测试问题及解决方案。

    2. 基础测试

    2.1. setup启动文件

    项目中如果使用了不需要测试的外部全局变量(如window对象),但却在node环境下运行测试,则可能抛出异常,对于这些全局变量,常见的做法是提前在node环境下准备好这些全局对象。

    而这种在启动测试用例前的准备工作可以放在启动文件中进行处理,往往命名为setup.js,而默认的vue add unit-jest不会创建该文件,需要我们自己创建并配置jest

    首先配置全局文件

    // jest.config.js,
    module.exports = {
      setupFiles: ['<rootDir>/tests/unit/setup.js'], // 指定setup的位置
      //...其他配置
     }
    }

    然后再tests/unit/下创建setup.js文件,最后在该文件中处理准备工作相关逻辑即可

    // <rootDir>/tests/unit/setup.js
    
    // mock全局对象appInfo
    window.appInfo = {
        name: 'shymean',
        version: '1.2.3'
    }

    在运行测试用例前会先运行setup中的代码,这样测试期间遇见这些全局变量就不会报错了

    2.2. 处理非js模块的依赖

    如果某个模块使用了非js模块(如css),则在测试该模块时会抛出异常,此时需要安装jest-transform-stub

    npm install --save-dev jest-transform-stub

    然后在jest配置文件中(项目根目录下的jest.config.js)通过moduleNameMapper配置项进行处理

    // jest.config.js,
    module.exports = {
      preset: '@vue/cli-plugin-unit-jest',
      transformIgnorePatterns: ['/node_modules/'],
      moduleNameMapper: {
        "^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
      }
    }

    stub是在测试中用来替代真正依赖对象的一种方式,除此之外还有fake、mock、spy等方式,在后面的具体测试场景中会提到

    2.3. 当前时间的处理

    在业务代码中,往往会存在类似的获取当前客户端时间并进行判断的逻辑,如

    function inActivityTime() {
        const startTime = '2020-10-01 00:00:00'
        const endTime = '2020-11-30 00:00:00'
        const now = moment() // 或者是 +new Date() 之类获取当前时间的方法
        return now >= moment(startTime) && now <= moment(endTime)
    }

    而在测试这个方法时,应需要编写多个时间点下的测试用例。从实现上来看,这个方法本身不便于测试,更合适的处理应当是将当前时间戳通过参数的形式传进入

    function inActivityTime(now) {
        const startTime = '2020-10-01 00:00:00'
        const endTime = '2020-11-30 00:00:00'
        return now >= moment(startTime) && now <= moment(endTime)
    }

    这样只需要通过控制参数,就能编写不同的测试用例了。

    it("inActivityTime 当前时间是否在活动时间内", ()=>{
        expect(inActivityTime('2020-09-01 00:00:00')).toBe(false)
        expect(inActivityTime('2020-12-01 00:00:00')).toBe(false)
        expect(inActivityTime('2020-11-01 00:00:00')).toBe(true)
    })

    设计是美好的,但现实往往是残酷的。一种现实是:在我们编写测试用例的时候,业务中可能已经存在大量类似的不便于测试的代码(从另外一方面证明了TDD的好处),所幸jest提供了一些模拟当前时间的方法,参考

    不过经过一番折腾之后,我发现了最简单的方式:使用mockdate这个库

    it("inActivityTime 当前时间是否在活动时间内", ()=>{
        MockDate.set('2020-09-01 00:00:00')
        expect(inActivityTime()).toBe(false)
    
        MockDate.set('2020-12-01 00:00:00')
        expect(inActivityTime('2020-12-01 00:00:00')).toBe(false)
    
        MockDate.set('2020-11-01 00:00:00')
        expect(inActivityTime('2020-11-01 00:00:00')).toBe(true)
    })

    2.4. 测试模块api和网络请求

    某些需要测试的功能会执行副作用代码,如在一个action中发送网络请求。

    就测试而言我们只希望验证这个方法是否正确调用,或者仅仅希望这个方法会按照期望返回数据或者抛出异常,而不希望发送真实的请求。这种场景可以使用jest.mock来处理

    假设现在api/user.js封装了所有用户信息相关的接口

    // api/user.js
    export const getUserInfo(){
        return request.get('/api/v1/user/info/')
    }

    首先使用jest.mock代理整个模块,然后通过mockResolvedValue模拟返回值即可

    import {getUserInfo} from '@/api/user'
    
    jest.mock('@/api/user')
    
    getUserInfo.mockResolvedValue({
      msg: 'SUCCESS',
      code: 200,
      data: {
          // ... mock user info
      }
    })

    假如是直接在某个地方里面通过axios.get('/xxx/xxx/url').then()之类调用网络接口,这种通过拦截请求接口模拟返回值的方法就不太适用了。从这一点也可以看见,测试驱动开发貌似更容易写出容易测试和维护的代码。

    getUserInfo.mockResolvedValue返回的本身是一个jest.fn()mock方法,他包含一些特定的属性和方法,比如

    • mockImplementationOnce直接重写整个方法的实现
    • mock.calls历史调用记录,可以通过mock.calls.length判断调用测试,通过mock.calls[0][0]获取第1次调用的第1个参数,诸如此类

    3. 测试store

    在笔者的项目中,大量的业务逻辑都会通过vuex放在store中按module进行拆分和处理,视图组件只负责展示和处理独立的逻辑。因此store是最需要优先进行测试的

      3.1. 测试mutation

      由于mutation只是接受state和payload的纯函数,因此测试起来十分方便,

      // store/modules/user.js
      export default {
          state:{
              userInfo:null
          },
          mutations:{
              setUserInfo(state, payload){
                  state.userInfo = payload
              }
          }
      }

      如果现在要测试这个setUserInfo,则直接测试这个模块的mutations.setUserInfo方法即可

      import user from '@/store/modules/user.js'
      
      it('setUserInfo 设置用户详情', () => {
          const { setUserInfo } = user.mutations
          let userInfo = { uid:1 }
          const state = { userInfo }
          setUserTrialScheduleDetail(state, userInfo)
          expect(state.userInfo).toBe(userInfo)
      })

      如果不是为了测试代码覆盖率,我个人认为像上面这种简单的mutation不需要测试,如果mutation还包含一些额外的计算,则可以按照函数单元测试的方法编写测试用例,验证参数和边界条件等。

      3.2. 测试getters

      getters本质上也是一个返回计算数据的纯函数,无非参数变成了state、getters、rootState、rootGetters,

      // store/modules/user.js
      export default {
          state:{
              userInfo:null
          },
          getters:{
              userAvatar(state){
                  if(!state.userInfo) return ''
                  return state.userInfo.avatar.replace(/https?:/, '')
              }
          }
      }

      对于简单的getter,也可以使用跟上面测试mutation的方法一样测试

      import user from '@/store/modules/user.js'
      
      it('userAvatar 获取用户头像', () => {
          const { userAvatar } = user.getters
      
          let userInfo = { avatar: 'http://shymean.com/avatar.png' }
          expect(userAvatar({ userInfo })).toBe('//shymean.com/avatar.png')
      
          expect(userAvatar({ userInfo:null })).toBe('')
      })

      如果需要getter或者rootState、rootGetters等参数,也可以通过构造对象的形式传入。

      在后面我们会介绍测试完整的store,而不是像现在这样测试单个独立的方法

      3.3. 测试action

      整个store模块中最麻烦的就是是测试action,因此action会处理各种异步逻辑,同时调用commit、dispatch等方法,包含比较复杂的业务逻辑

      // store/modules/user.js
      export default {
          state:{
              userInfo:null
          },
          mutations:{
              setUserInfo(state, payload){
                  state.userInfo = payload
              }
          },
          actions:{
              async fetchUserInfo({commit}){
                  const {data} = await getUserInfo()
                  commit('setUserInfo', data)
              }
          }
      }

      对于上面这个fetchUserInfo而言,我们需要测试的是

      • 内部调用了getUserInfo这个接口,并异步获取到返回值
      • 内部调用了commit('setUserInfo'),更新state

      从测试的角度来说,我们不需要发送真实的getUserInfo请求,而是期望检测getUserInfo被调用,并将返回值的data属性作为参数传递给commit('setUserInfo'),因此可以这样写测试用例

      // 使用async 返回一个Promise
      it("fetchUserInfo 获取用户基本信息", async () => {
          const { fetchUserInfo } = user.actions;
      
          const userInfo = { uid: 1 };
          getUserInfo.mockResolvedValueOnce({
              msg: "SUCCESS",
              code: 200,
              data: userInfo,
          });
      
          const mockCommit = jest.fn(() => {});
      
          await fetchUserInfo({ commit: mockCommit });
      
          expect(mockCommit.mock.calls.length).toBe(1)
          expect(mockCommit.mock.calls[0][0]).toBe('setUserInfo')
          expect(mockCommit.mock.calls[0][1]).toBe(userInfo)
      });

      3.4. 测试完整的store

      上面简单介绍了分别单元化mutations、getters和actions的测试,这种做法的好处是测试用例非常详细,但由于state、commit等参数都是模拟的,可能无法保证整个系统的可靠性测试。接下来介绍通过store.commit、store.dispatch等来测试一个真正运行的store

      为了保证每个测试用例在单个纯净的store,需要使用createLocalVue创建新的store

      import { createLocalVue } from '@vue/test-utils'
      import storeConfig from '@/store/config'
      
      let store
      beforeAll(() => {
        const localVue = createLocalVue()
        localVue.use(Vuex)
        store = new Vuex.Store(cloneDeep(storeConfig))
      })

      这样可以保证单个测试用例的

      以上面的getters.userAvatar为例,正常逻辑的话需要先设置state.userInfo,然后测试getters.userAvatar的值

      it('userAvatar 获取用户头像', () => {
          store.commit('user/setUserInfo', { avatar: 'http://shymean.com/avatar.png' })
          expect(store.getters['user/userAvatar']).toBe('//shymean.com/avatar.png')
      
          store.commit('user/setUserInfo', null)
          expect(store.getters['user/userAvatar']).toBe('')
      })

      这样,就可以通过常规的store数据流来控制并测试。在实际编写测试用例期间,也遇见一些不容易测试的store代码,最常见的就是某些方法里面依赖了全局变量。

      4. 测试Vue组件

      测试组件最基本的思路就是:把测试目标组件之外的其他信息都给屏蔽掉,只测试当前组件的相关逻辑

      4.1. mock外部变量

      由于Vue组件本身也是一个JS模块,因此可以使用颗粒化的数据来测试对于组件自身的computed、methods等属性,

      • 测试computed是否返回了预期的计算属性
      • 测试methods是否执行了预期的逻辑,比如调用函数、dispacth对应action等

      然而现实总是残酷的,组件除了自己的data、computed、methods和生命周期函数之外,往往还依赖一些外部变量,如

      • 插件注册到vue实例上的属性如vuex的$store、vue-router的$router和$route,ElementUI的$message等原型方法
      • props、provide通过父组件或组件组件注入的数据
      • 其他游离在组件作用域之外的自由变量

      就测试而言,对于这些外部变量,最常规的做法就是将其mock掉,只测试组件自身的逻辑,参考:mount和shallowMount配置参数

      const wrapper = shallowMount(TestComponent, {
          mocks: {
              $route: {query:{}}
          }
      })

      需要注意的是,某些插件如vue-router等在Vue.use(VueRouter)之后,会添加只读的$route属性到vue实例上,这种情况下如果通过mount或shallowMount传入mocks配置项模拟$route则会抛出异常

      [vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property as a read-only value

      一种解决办法是在Vue.use(VueRouter)处添加环境变量判断,当运行测试用例时不执行该逻辑;另外一种解决办法时将该逻辑移动到测试用例不会加载的依赖模块之外进行注册,比如放在main.js入口文件等地方

      其解决思想就是避免在这些插件添加只读属性导致mock失败,然后再通过传入mocks配置项模拟$route等外部数据

      除了外部变量之外,在比较长的方法中可能还存在诸如内联函数等无法在测试代码中访问到的变量,对于这种变量,除了通过mockImplementationOnce模拟实现获取参数来测试之外,目前没有找到比较好的测试方法,感觉最好的办法是编写易于测试的代码

      4.2. 忽略子组件

      整个Vue应用由组件构成,一个组件内部往往会依赖其他组件。为了保证测试边界清晰,只希望测试当前组件,忽略所依赖的子组件,则可以通过shallowMount结合jest.mock(子组件模块)来实现

      <template>
          <div class="page">
              <div class="page_tt">parent</p>
              <Children></Children>
          </div>
      
      </template>

      可以将整个children组件模块给mock调

      jest.mock('./children.vue', () => ({
        render(h) {
          h(); // 模拟子组件,并返回一个空节点
        },
      }));
      
      const wrapper = shallowMount(Parent, {});
      
      it('标题渲染正确', () => {
          expect(wrapper.find('.page_tt').text()).toEqual('parent');
      });

      5. 小结

      本文主要整理了如何通过vue-test-utils测试vue代码,包括

      • 基础的测试功能,包括处理全局逻辑,当前时间及依赖模块API
      • 测试store,颗粒化测试单个mutation和action,以及整体测试store
      • 测试Vue组件,包括通过mock配置项处理外部依赖、忽略子组件等
      原文:https://www.shymean.com/article/在vue项目中使用单元测试

      链接: https://fly63.com/article/detial/10570

      测试工具比较:选Jest,不选Mocha

      Jest的未来看起来非常令人激动!看到Jest推陈出新如此快速,我感觉它将很快成为整个React生态系统中大部分项目的首选工具。我建议,应该把测试迁移到Jest上去。

      你需要了解的前端测试“金字塔”

      如果您正在测试前端应用程序,则应该了解前端测试金字塔。在本文中,我们将看到前端测试金字塔是什么,以及如何使用它来创建全面的测试套件。

      web网页性能测试工具都有哪些

      作为前端开发,我们不仅需要满足产品需求功能的实现,同时也需要对自己做的网站进行安全、易用性、性能等方面的考虑。随着目前技术不断进步,web页面的性能测试工具也在不断完善,通过这些工具,我们可以客观的评价web网站的质量水平。

      js单元测试工具-jest自动化测试

      jest 是 facebook 开源的,用来进行单元测试的框架,可以测试 javascipt 和 react。jest 提供了非常方便的 API,可以对下面的场景方便的测试:一般函数、异步函数、测试的生命周期、react 测试

      web测试要点、方法_web端测试大全总结

      web测试大全,测试web网站有哪些点呢?主要包括:功能测试、兼容性测试、安全测试、输入框测试、用户权限测试等

      前端性能测试工具整理简介_性能测试工具都有哪些?

      前端性能测试工具都有哪些:Favicon、Open Graph、图片优化-压缩图像、CSS 优化-Autoprefixer、Purifycss、minify CSS、减少载入时间、GZIP、CDN、优化平台-Sentry、Google Tag Manager

      不用写代码,也能做好接口测试

      本文你将了解到:1、接口测试基本概念,包含什么是接口,什么是接口测试,为什么要做接口测试;2、接口测试用例设计,3、怎样不用写代码,也能快速的根据开发的API文档完成接口自动化测试脚本

      Selenium打开浏览器加载慢的原因

      在自动化元素定位操作中经常使用智能等待来加强定位的强壮性,主要就是因为WebDriver没有提供页面加载场景的方法;在使用JavaScript知识的突然心生灵感,可以使用JavaScript来配合验证页面加载,结果发现我真是井底之蛙。

      power assert_更智能、优雅的全方位 assert 断言库

      在写测试代码时,以往我们需要翻阅文档,学习各种 API 才能明白如何操作断言。而现在我们可以透过 power-assert 的 assert 方法来减轻调试压力。不仅如此,它还提供更加直观,具体的运行效果,帮助 DEBUG。写测试代码,其实可以很容易。

      常用的web网站负载/压力/性能测试工具

      在网站上线发布之前,我们除了必要的安全、功能测试外,往往还需要进行压力测试。通过模拟实际应用的软硬件环境及用户使用过程的系统负荷,长时间或超大负荷地运行测试软件。包括:Apache JMeter 、LoadRunner、NeoLoad等

      点击更多...

      内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!