GithubHelp home page GithubHelp logo

holylovelqq / vue-unit-test-with-jest Goto Github PK

View Code? Open in Web Editor NEW
487.0 5.0 90.0 5.45 MB

吃透本仓库,变身vue项目单体测试大神

JavaScript 6.65% HTML 52.21% Vue 3.28% CSS 35.18% TypeScript 2.68%

vue-unit-test-with-jest's Introduction

vue-unitTest-with-jest

收录于ruanyf/weekly-55期

🎉🎉🎉 进阶版已更新完成,建议零基础的看完基础篇再看进阶篇🎉🎉🎉

  • 任何教程都不会是完美的,如发现任何不足之处欢迎提issue,另外issue里面的笔记非常推荐阅读
  • 该仓库目标是实现从零开始到单元测试无障碍的进阶。
  • 列举了所有可能出现在vue项目中的,需要进行测试的项。可与实际项目中一一对应
  • 本仓库内都是基础的测试用例,为了能更好理解,伪造的例子都比较简单。实际项目中需要在理解本仓库测试思路的前提下,根据项目实际情况写测试。
  • 覆盖率是用来衡量代码质量的标准,越是复杂的项目要求的覆盖率越低。
  • 本仓库内未进行测试的【router,第三方插件等】并不是绝对不需要测试,如果有单独对插件和router进行设置的话,设置部分是需要测试的。也就是说第三方插件本身我们默认是经过严密测试的,而我们添加的逻辑部分需要自己添加测试。
  • 零散的知识点都以笔记的形式添加在issue了,可以参考
  • 测试代码也要遵循项目的代码规范,和代码书写的基本修养,比如:
expect(wrapper.vm.mockFn).toBeCalled();
expect(wrapper.vm.mockFn).toBeCalledWith('xx'); 

上面测试代码看似没有问题,其实toBeCalledWith('xx')就能保证toBeCalled(),所以toBeCalled()这一行应该删除。但是本仓库基于教学的目的并没有这么做,而是保留了。

代码注释很多基本都能看懂,文档会包括一些概括的说明,是代码中未能体现的

vue-unit-test-with-jest's People

Contributors

dependabot[bot] avatar holylovelqq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

vue-unit-test-with-jest's Issues

【笔记】单元测试的意义

网上搜一搜,看看大神么都是怎么说的
个人觉得写了单测以后对自己的影响

  1. 重新审视自己的代码,这句话比较宽泛,但却是如此,其中包括组件的布局分割组成文件以及注释写法,什么时候可以不写注释等等,全方面的规范化影响

スクリーンショット 2019-09-24 12 07 21

  1. 了解每一行代码存在的意义,有效减少冗余代码,且单元测试要求你测试你代码的所有情况,有效避免逻辑错误
  2. 规范代码,提高代码质量。与前面重复,但是值得单独作为一条

router的单元测试用例会报错

[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

使用

wrapper = shallowMount(MessageTip, {
      mocks: {
        $route,
        $router
      }
    })

运行测试时会报错

请教大佬

nuxt+ts框架下组件内使用了mixins,使用jest测试提示找不到mixins中定义的方法,尝试了使用createLocalVue().mixin(mymixins)不起作用,请教大佬这个改怎么写,

请问如何测试一个指令?

官网没看到相关例子。
我有如下代码:

<template>
  <div>
    <h2>测试全局指令</h2>
    <Button v-auth:disabled="'li'" type="primary">无权限时禁用按钮</Button>
    <!-- 无权限时删除按钮 -->
    <Button v-auth="'li'" type="button">无权限就从页面移除按钮</Button>
    <br />
    <h3>测试 v-click-outside</h3>
    <div v-click-outside="onClickOutside">
      <p>测试点击外部</p>
      <p>测试点击外部</p>
      <p>测试点击外部</p>
      <p>测试点击外部</p>
    </div>
  </div>
</template>

<script>
import { Button } from 'element-ui'
export default {
  name: 'WebComponent',
  components: {
    Button,
  },
  data() {
    return {
      msg: 'Hello web components in stencil!',
      maxValue: 5,
      value: 2,
      persons: [{ name: 'jack', age: 30 }],
    }
  },
  methods: {
    onClickOutside() {
      console.log('onClickOutside')
      console.log(this.msg)
    },
  },
}
</script>

我的指令代码:

const $auth = function (key = '') {
  // NOTE 从后台获取到的权限列表,放在store里面
  const buttonKeys = ['key', 'key1', 'key2']
  // 返回 bool ,方便在结合 v-if 使用
  return buttonKeys.includes(key)
}

export const auth = {
  install(Vue, options = { disabled: true }) {
    Vue.directive('auth', {
      inserted(el, bindings) {
        const { value: key = '', arg = 'show' } = bindings
        if (key && !$auth(key)) {
          // 1. 无权限时只是禁用,用户可以编辑页面启用,且可出发事件
          // NOTE 解决办法:克隆一个新元素,使用新元素代替旧元素
          // cloneNode(true) 克隆整个节点,通过 addEventListener 绑定的事件不会被克隆
          // NOTE 行内属性绑定的事件会被克隆
          // 2. 有的元素没有 disabled 属性,设置了没有用
          // NOTE pointer-events:none 可实现任意元素禁用 hover 和 click
          // 缺点:hover 时不显示 title

          // 无权限时禁用按钮
          if (arg === 'disabled') {
            // el.disabled = true
            // setAttribute 能新增不存在的属性
            el.setAttribute('disabled', true)
            el.title = '没有操作权限'
            // 设置无权限的样式
            el.style['pointer-events'] = 'none'
            el.style.opacity = 0.6 // 设置该属性只是起到禁用效果而已
            el.style.cursor = 'not-allowed'
            const elClone = el.cloneNode(true)
            el.parentElement.replaceChild(elClone, el)
          } else {
            // 无权限时删除按钮
            el.remove()
          }
        }
      },
    })
  },
}

export const clickOutside = (Vue, options) => {
  Vue.directive('clickOutside', {
    inserted(el, bindings, vnode) {
      console.log('inserted')
      console.log(bindings)
      const { value } = bindings
      // const _this = vnode.context
      // NOTE 技巧:处理函数挂载在元素上,方便解绑时移出事件监听
      el.onClick = ({ target }) => {
        // 点击的元素包含在某个日历内,选择面板出现,否则面板消失
        if (el.contains(target)) {
          // 点击内部
          console.log('clickInside')
          // _this.focus()
        } else {
          // 点击外部
          console.log('clickOutside')
          // _this.blur()
          // _this.onClickOutside()
          value && value()
        }
      }
      document.addEventListener('click', el.onClick, false)
    },
    unbind(el, bindings, vnode) {
      console.log('unbind')
      document.removeEventListener('click', el.onClick, false)
    },
  })
}

大佬,请问如何测试v-click-outside?

请教一些关于测试的问题

  1. 我们应该是按照组件的功能(类似 element 框架)还是环节(props,filter...)进行测试?

  2. props 有测试的必要性吗?(props 足够简单,大多数情况下没必要测试,当然追求覆盖率除外)

  3. 除了手动操作 dom 外,组件的 data、props 和 computed 都是直接作用于 dom,作为变化源这三个环节应该去测试 dom,而不是数据(这一块儿本身可以省去),原因是数据测试通过,dom 并不一定符合预期

很感谢您的贡献,让我用了极少的精力摸了一遍 vue 测试的各种套路。

最后,祝您工作顺利,生活愉快。

【笔记】关于定时器/异步函数的测试方法

请先参考官网介绍,这段主要是介绍的测试定时器的几个api

jest.useFakeTimers() jest.runAllTimers()

请看下方示例:

describe('methods.searchLazy()', () => {
    jest.useFakeTimers();

    it('500ms后更改搜索关键字', () => {
      const push = jest.fn();
      mountOptions.methods = { push };
      wrapper = shallowMount(BaseTable, mountOptions);
      wrapper.vm.searchLazy('hello');
      jest.runAllTimers();
      expect(push).toBeCalledWith({ search: 'hello' });
    });
  });

另外测试代码中应该尽量避免出现nextTick,可以使用上记的针对nextTick的api实现,如果有其他更简单的方法就更好

  • 测试异步行为参考Vue Test Utils
    【追加】感觉这里一句话带过异步测试似乎有点轻率了,实际测试中会经常遇到异步测试,而具体用到的方法也都在上面的链接里提到了,也没有什么需要额外说明的,这里提一下flush-promises插件吧,如果到了必须用插件的地步的话,这个还是可用的

trigger('click')检测自身绑定的函数被触发

正如#33 那样,setMethods被废弃,同时#31const wrapper = shallowMount(AppButton , {methods: {onClick: mockFn,}})这种形式也被废弃,那如何修改代码使得AppButton.spec.js下的click button onClick is clled这个用例通过


至于jest.spyOn(),目前我试出来的结果只能在函数依赖另一个函数时mock掉被依赖的函数,假设

// Test.vue
<template>
  <button @click="clickMethod">button</button>
</template>

<script>
export default {
  name: "Test",
  methods: {
    clickMethod() {
      this.dependencyFun();
    },
    dependencyFun() {
      console.log(123);
    },
  },
};
</script>

测试mockdependencyFun,通过

it("can trigger click", async () => {
    const wrapper = shallowMount(Test)
    const mockFn = jest.spyOn(wrapper.vm, "dependencyFun").mockName("dependencyFun")
    await wrapper.find("button").trigger("click")
    expect(mockFn).toBeCalled()
    expect(mockFn).toHaveBeenCalledTimes(1)
})

测试mockclickMethod,失败

it("can trigger click", async () => {
    const wrapper = shallowMount(Test)
    const mockFn = jest.spyOn(wrapper.vm, "clickMethod").mockName("clickMethod")
    await wrapper.find("button").trigger("click")
    expect(mockFn).toBeCalled()
    expect(mockFn).toHaveBeenCalledTimes(1)
})

但是这样写又能通过

it("can trigger click", async () => {
    const wrapper = shallowMount(Test)
    const mockFn = jest.fn().mockName("clickMethod")
    wrapper.vm.clickMethod = mockFn
    await wrapper.vm.clickMethod()
    expect(mockFn).toBeCalled()
    expect(mockFn).toHaveBeenCalledTimes(1)
})

非常不解,不明白这是怎么回事

關於官方將移除 wrapper.setMethods

查到應該替換成爲:

import Sample from '@/views/Sample.vue'

describe('test', () => {
  it('test function', async () => {
    const wrapper = shallmount(Sample)
    // TypeScript 下需要寫成 jest.spyon(wrapper.vm, 'myFunction' as any)
    const mockFunction = jest.spyon(wrapper.vm, 'myFunction')
    
    await wrapper.vm.myFunction()
    
    expect(mockFunction).toBeCalled()
    
    wrapper.destory()
  })
})

【笔记】mount与shallowMount的区别

结论写在最开始:尽一切努力使用shallowMount,远离mount

在Vue Test Utils官网的shallowMount常用技巧中分别都提到了下面的话

和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,不同的是被存根的子组件。

Vue Test Utils 允许你通过 shallowMount 方法只挂载一个组件而不渲染其子组件 (即保留它们的存根)

这已经是两者区别的官网介绍的全部。我敢肯定只看这两句话肯定是一头雾水,任你vue技术再好水平再高,不通过实际单测实践,是完全不能理解其中的奥秘的。

此篇笔记将揭开mount和shallowMount之间不可告人的秘密
写这种零散笔记完全是因为没有时间更新仓库内容,对单测有兴趣的就将就着看吧。

  1. 渲染结果:
    1. mount:踏踏实实的渲染,会将被测试组件中使用到的子子孙孙组件完全渲染。最终结果内肯定不存在自定义组件名作为标签名,包括插件提供的V-btn之类的dom结构,全部不存在,彻底渲染到<div><span><p>之类
    2. shallowMount:浅渲染,与mount相反,既不会渲染子组件,更不用提孙子辈的组件,仅限测试组件本身。子组件会原原本本的显示子组件的存根,当使用stubs的时候,子组件存根是可以自定义显示内容的,如果未自定义,将会显示子组件名-stub的标签名,如<my-componet-stub></my-componet-stub>

渲染结果总结

mount刨根问底,祖宗十八代都不会放过,shallowMount仅限测试组件,不牵扯子组件内容

  1. 使用场景:
    1. mount:多数情况出现在snapshot中使用,而且也只是部分snapshot中必须使用mount。下面将说明何种情况:当被测试组件内包含子组件,且子组件的dom结构通过slot在被测试组件内定义的时候,必须使用mount来测试snapshot。
追加说明:此种情况也是可以使用shallowMount的,做法为:
import myComponent from '~/myComponent'
...
options:{
  ...
  stubs:{ myComponent }
}
//被测试组件内
...
<my-component>
  <template #body>
    <div>
      自定义内容
    </div>
  </template>
</my-component>
...
  1. shallowMount:所有必须使用mount的情况以外都使用shallowMount,使用mount将更大的消耗时间和性能,
    ----> 实际上所有测试都可以不使用mount,但是鉴于测试的复杂性,有可能会出现用shallowMount怎么也报错的情况,但是改成mount就能测试通过

使用场景总结

基本准则:所有使用shallowMount的地方都可以用mount替换,但是但凡能使用shallowMount的地方坚决不用mount;

无脑做法:snapshot测试中都使用mount,其他测试都使用shallowMount,测试不会太慢,但也不是最快
【追加】 基于编程的严谨性,非常不提倡这种无脑做法,建议全部使用shallowMount。且上面提到的必须使用mount的情况实际也是非必须的,对此也追加了说明

  1. 与stubs的关系
    1. mount:可以与stubs同时使用,但是属于小概率事件。首先不得不使用mount的情况就很少,在此基础上又不得不使用stubs的概率就更小了。
    2. shallowMount:多数使用stubs的情况都是跟shallowMount一起,但是并不是使用shallowMount就一定需要stubs。shallowMount本身就是使用子组件的存根创建wrapper,但是使用了stubs的存根和没有stubs的存根是有区别的,这个会在下面单独讲

与stubs的总结

经常与shallowMount一起出现,只在不得不的场合才会跟mount一起出现。

  1. shallowMount与stubs那些事(参照Vue Test Utils-配置
    1. shallowMount不与stubs合作,单独渲染,此时将使用存储在@vue/test-utils下config.stubs中的默认存根渲染dom
    2. 与stubs合作:config.stubs下的存根会被stubs内存根覆盖,依然是使用该存根
      1. stubs内定义了存根内容,根据自定义内容渲染结果
      2. stubs内未定义存根内容,如果是空数组,毫无意义,跟没写一样。如果是stubs:['may-component']只声明了一个名字,则将may-component解析为一个空的存根,与默认存根不同哦
      3. 默认存根与空存根的不同:空存根只会渲染实际传递的props,默认存根是会将存在默认值的props全都渲染。目前仅发现这点不同,请不要小看这点不同,如果你使用ts的话,这会直接影响你的snapshot测试结果

shallowMount与stubs

stubs存在的话优先按照stubs内容渲染,不存在使用默认存根渲染。注意空存根与默认存根的区别

断言vue生命周期中是否触发某个函数该怎么处理

组件
mounted () { this.checkLoginState(); }, methods: { checkLoginState () { if (this.hasLogin) { this.$router.push("/goods") return; } } }
测试代码
const wrapper = shallowMount(Home) const checkLoginStateStub = jest.spyOn(wrapper.vm, "checkLoginState"); expect(checkLoginStateStub).toHaveBeenCalled();
这样会断言失败,请问该怎么处理呢

更新预定

鉴于现存的教程及代码主要面对初学者,貌似不能达到实际中灵活运用的目的,故预定接下来的更新如下

  1. 整合仓库布局
    • 大致分为【初学入门】和【高级进阶】两个文件夹
    • 现存的代码保留,作为【初学入门】教程实例
    • 寻找网络上现有未写单测的开源项目(或者其他可使用项目)添加单测,作为【高级进阶】的教程实例、如果采取收费模式(见下说明)的话,就只能选择自己以前的练手项目拿来做模版了。比较麻烦的将会是选择合适的项目。
  2. 现存的代码对于完全0基础的单测入门教程还是比较合适的,简单易懂,涵盖面较广,有助于打基础,如果用心学习,再加上能在实际项目中实践的话(当然最好是有大神带),个人感觉可以完全不用看高级进阶教程。
  3. 【高级进阶】----主要更新内容
    • 教程目的:学完以后能独立处理可能出现的单测情况的90%,剩下10%考验的是能否灵活运用,以及对各种复杂情况的应变能力。但是鉴于每个人的吸收能力不同,也不能完全保证。
    • 暂定内容
      • 详解实际项目中的单元测试思路(找准出发点,明确目的,规范代码)
      • 详解实际项目中jest的配置/覆盖率解读(整合网上现有资源,结合经验)
      • 详解snapshot测试原理(理解官网介绍,简单易懂的总结,结合实例)
      • 详解axios单测写法(有别于【初级入门】)
      • 异步测试详解(使用插件flush-promises与不用的写法)
      • vuex测试详解
      • store测试详解
      • vue-router测试详解(一般不需测试)
      • 官方api详解
        • shallowMount与mount #4
        • stubs/mocks/data/methods等
  4. 预定完成日:19年内 推迟,日期未定
  5. 鉴于高级进阶教程内容可能会比较多,比较杂,暂定采取收费模式,目的是能强化其重要性。最终要根据教程内容与完成度决定是否采取收费模式,即便收费也不会太高
  6. 现存代码基本不做大的调整,教程说明文字有可能会做修改。【高级进阶】尽量实现规范化,增强commit的可读性
  7. 目测vue3.0有很大可能也在元旦(或春节)前后推出。从现阶段已经公开的信息来看似乎对测试没有影响,但是也不绝对。如果3.0推出后证明对测试有影响的话,这边也应该及时更新

---随时修改完善---

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.