掌握vue全家桶单元测试:深入理解组件测试

2612 篇文章 2 订阅
2449 篇文章 14 订阅

前置知识

观念改变

在这一章,会讲到如何测试组件,我在这一章只讲组件测试的基本操作,不会去讲测试的心法,后面章节再专门讲测试的心法,我会带着大家,把复杂的业务点,拆成一个一个细点,让大家先把测试用例写出来,后面再考虑写不写得好。就好像先学 typescript 基本的类型操作就能覆盖日常开发的 90%,想把 typescript 写的更好,那就得学typescript 体操,而 typescript 体操平时开发可能只占不到 10%

异步

在JavaScript中执行异步代码是很常见的,我们前面学的 it 上下文,是一个同步的上下文,在我们测试组件的时候,尤其是 vue 更新 setData 的时候,是异步的。这就需要在一个异步的 it 上下文中执行断言代码

之前我们学过的同步上下文:

  it('mount first render', () => {
    })

异步上下文只需要加一个 async,然后就可以使用 await,类似 js 的 async await 用法一样:

  it('update multiple render', async () => {
      // async await
      await nextTick()
    })

如何设置 Data

我们之前说了 mount 和 shallowMounnt, 其实 mount 还有第二个参数,可以设置

  <script setup lang="ts">
  import { ref } from 'vue'
  const str = ref('')
  </script>
  <template>
    <div>{{ str }}</div>
  </template>
  it('mount', async () => {
       const wrapper = mount(Data, {
        setup() {
          return {
            str: 'first render'
          }
        }
      })
      expect(wrapper.text()).toContain('first render')
  })

但我们日常业务还是极少会手动设置 data,往往是mount勾子里面去修改data

如何解决 mount 之后的数据变化?

  <script setup lang="ts">
  import { onMounted, ref } from 'vue'
  const str = ref('first render')
  onMounted(()=>{
    str.value = 'second render'
  })
</script>
  <template>
    <div>{{ str }}</div>
  </template>

因为勾子函数异步更新了 str,所以需要使用await nextTick()等待更新完成之后再断言

  it('mount', async () => {
      const wrapper = mount(Data, {})
      expect(wrapper.text()).toContain('first render')
      await nextTick()
      console.log('wrapper.text()', wrapper.html())
      expect(wrapper.text()).toContain('second render')
    })

如何临时修改组件的值?

 it('mount', async () => {
      const wrapper = mount(Data, {})
      expect(wrapper.text()).toContain('first render')
      await nextTick()
      expect(wrapper.text()).toContain('second render')
      wrapper.vm.str = 'third render'
      await nextTick()
      console.log('wrapper.text()', wrapper.html())
      expect(wrapper.vm.str).toBe('third render') // data 有没有值
      expect(wrapper.text()).toContain('third render') // data 是否正确渲染在页面上
    })

props

  <script setup lang="ts">
  defineProps<{
    msg: string
  }>()
</script>
  <template>
    <div>{{ msg }}</div>
  </template>
  it('mount props', async () => {
      const wrapper = mount(Props, {
        props: {
          msg: 'props msg'
        }
      })
      expect(wrapper.text()).toContain('props msg')
    })

动态 setProps

  it('update props', async () => {
      const wrapper = mount(Props, {
        props: {
          msg: 'props msg'
        }
      })
      expect(wrapper.text()).toContain('props msg')
      await wrapper.setProps({
        msg: 'second render'
      })
      expect(wrapper.props('msg')).toBe('second render') // props 有没有值
      expect(wrapper.text()).toContain('second render') // props 是否正确渲染在页面上
    })

日志输出

 console.log('wrapper',wrapper.props('msg'))

如何测试 emits

要修改和添加案例:

 <script setup lang="ts">
  interface Emits {
    (e: 'change', value: string)
    (e: 'update:pageIndex', value: number)
    (e: 'update:pageSize', value: string, size: number)
  }
  const emits = defineEmits<Emits>()
  const resetPage = (value: string) => {
    emits('update:pageSize', value, 10)
    emits('update:pageIndex', 1)
    emits('change', value)
  }
  </script>
  <template>
    <div @click="resetPage('customer')" data-testid="button">button</div>
  </template>
  it('mount', async () => {
      const wrapper = mount(Emitted)
      const button = wrapper.find('[data-testid="button"]')
      await button.trigger('click')
      const emits = wrapper.emitted()
      console.log('emits', emits)
      // emits {
      //   'update:pageSize': [ [ 'customer', 10 ] ],
      //   'update:pageIndex': [ [ 1 ] ],
      //   change: [ [ 'customer' ] ],
      //   click: [ [ [MouseEvent] ] ]
      // }
      expect(emits).toHaveProperty('update:pageIndex')
      expect(emits).toHaveProperty('update:pageSize')
      expect(emits).toHaveProperty('change')
    })

wrapper.emitted() 是一个数组,可以获取到 emit 事件的记录,根据数组里面的内容去断言。

provide/inject

父组件向下传递 this is parent data

Parent.vue

  provide('parentValue', 'this is parent data')

按钮组件拿到parent 传递过来的 parentValue 值

Button.vue

  const text = inject('parentValue')
  <div> {{ text }}</div>
  describe('测试 provide', () => {
    it('测试顶层组件渲染正确传递值给子组件', async () => {
      const wrapper = mount(Parent)
      expect(wrapper.text()).toContain('this is parent data')
    })
    it('测试子组件能拿到顶层组件传递的值', async () => {
      const wrapper = mount(Button,{
        global: {
          provide: {
            parentValue: 'test provide'
          }
        }
      })
      expect(wrapper.text()).toContain('test provide')
    })
  })

directive

从业务角度,从组件的角度:

  <script setup lang="ts">
  const vTooltip = {
    beforeMount(el: Element) {
      el.classList.add('with-tooltip')
    }
  }
</script>
  <template>
    <div>
      <div v-tooltip data-testid="tooltip">show tooltip</div>
    </div>
  </template>
  it('tooltip', async () => {
      const wrapper = mount(Directive)
      const tooltip = wrapper.find('[data-testid="tooltip"]')
      expect(tooltip.html()).toContain('with-tooltip')
    })

如果是全局的自定义指令,就得如下写法,需要在 main.ts 里面全局定义指令

  // main.ts
  app.directive('tooltip', vTooltip)
  <script setup lang="ts">
</script>
  <template>
    <div>
      <div v-tooltip data-testid="tooltip">show tooltip</div>
    </div>
  </template>

测试用例需要 directives 注入进来:

  it('tooltip', async () => {
      const wrapper = mount(Directive, {
        global: {
          directives: {
            tooltip: vTooltip
          }
        }
      })
      const tooltip = wrapper.find('[data-testid="tooltip"]')
      expect(tooltip.html()).toContain('with-tooltip')
    })

components

当我们在使用一些第三方组件的时候,可能第三方组件就是全局注册的,我们就不需要在每一个组件里面每次引入使用的组件,例如我有个 GlobalComponent 组件,被全局注册了

  import GlobalComponent from './components/6/GlobalComponent.vue'
  app.component('GlobalComponent', GlobalComponent)

那么在使用的时候就不需要在当前组件引入了,直接使用就行

 <template>
    <div>
      <GlobalComponent></GlobalComponent>
    </div>
  </template>
  <script setup lang="ts">
</script>

我们直接 mount 一下这个:

    it('mount error component', async () => {
      const wrapper = mount(Global)
      console.log(wrapper.html())
      expect(wrapper.text()).toContain('My Global Component')
    })

  需要在测试的时候,把组件注册进去:

  it('mount success component', async () => {
      const wrapper = mount(Global, {
        global: {
          components: {
            GlobalComponent
          }
        }
      })
      expect(wrapper.text()).toContain('My Global Component')
    })

plugins

我们再来看看 plugins, 插件很常见,vuex、vue-router 都是插件,我们如何测试插件呢,我列了一个平常可能会使用到的 i18n 插件,

  // i18n.ts
  const i18nPlugin = {
    install(app: any, options: PluginOptions = {}) {
      const messages = options.messages ?? {}
      app.config.globalProperties.$t = function (key: string) {
        const language = options.defaultLanguage ?? 'en'
        return messages[language]?.[key] || key
      }
    }
  }
  <!-- Plugin.vue -->
  <template>
    <div>{{ $t('hello') }}</div>
  </template>
  <script setup lang="ts">
</script>

main.ts 里面需要注册插件,这里我们直接定义 hello 的值是 'Hello Plugin'

  app.use(i18nPlugin, {
    defaultLanguage: 'en',
    messages: {
      en: {
        hello: 'Hello Plugin'
      }
    }
  })
  describe('测试 plugin', () => {
    it('uses i18n plugin', () => {
      const wrapper = mount(Plugin, {
        global: {
          plugins: [
            [
              i18nPlugin,
              {
                defaultLanguage: 'en',
                messages: {
                  en: {
                    hello: 'Hello test i18nPlugin'
                  }
                }
              }
            ]
          ]
        }
      })
      expect(wrapper.text()).toBe('Hello test i18nPlugin')
    })
  })

attachTo

我们有时候会在组件里面直接操作 dom,例如下面一个组件一开始渲染了 first render onMounted之后,获取 dom 之后,直接把h4里的内容改成111

  <script setup lang="ts">
  import { onMounted } from 'vue'
  onMounted(() => {
    const ele = document.querySelector('h4') as Element
    ele.innerHTML = '111'
  })
</script>
  <template>
    <h4 style="color: red">first render</h4>
  </template>

如果不使用 attachTo, 会渲染报错。

   it('attach render error', async () => {
      const wrapper = mount(Attach)
      await nextTick()
      expect(wrapper.text()).toContain('111')
    })

  正确的用法是 attachTo 到 body 上面或者其他的 DOM 上。

   it('attach success render', async () => {
      // const div = document.createElement('div')
      // document.body.appendChild(div)
      const wrapper = mount(Attach, {
        attachTo: document.body
        // attachTo: div // 任意一个 dom
      })
      await nextTick()
      console.log('wrapper', wrapper.html())
      expect(wrapper.text()).toContain('111')
    })

teleport

Vue 3 配备了一个新的内置组件:它允许组件将其内容 "传送 "到其自身之外,当我们直接 mount 一个包含 teleport 组件的时候,是不会展示具体内容的,只会显示

  <!--teleport start-->
  <!--teleport end-->

如何测试呢?

  MyTeleport.Vue 组件使用了  Teleport 功能,Teleport 包括了一个子组件 Signup.vue
  <template>
    <Teleport to="#modal">下面渲染子组件 <Signup></Signup> </Teleport>
  </template>
  <script lang="ts" setup>
  import Signup from './Signup.vue'
</script>

Signup.vue 是一个表单,用于验证用户名是否大于 8 个字符。如果大于 8 个字符,就把输入的用户名 emit 到父组件。

  <template>
    <div>
      <form @submit.prevent="submit">
        <input v-model="username" />
      </form>
    </div>
  </template>
  <script lang="ts" setup>
  import { ref, computed, defineEmits } from 'vue'
  const emit = defineEmits(['signup'])
  const username = ref('')
  const error = computed(() => {
    return username.value.length < 8
  })
  const submit = () => {
    if (!error.value) {
      emit('signup', username.value)
    }
  }
</script>

因为 teleport  到了当前组件的外部,需要额外使用 getComponent?或者?findComponent来直接获取 Signup.vue,然后再进行相关的断言操作

  beforeEach(() => {
    const el = document.createElement('div')
    el.id = 'modal'
    document.body.appendChild(el)
  })
  afterEach(() => {
    document.body.outerHTML = ''
  })
  test('teleport', async () => {
    const wrapper = mount(Teleport)
    console.log(wrapper.html())
    const signup = wrapper.getComponent(Signup)
    await signup.get('input').setValue('valid_username')
    await signup.get('form').trigger('submit.prevent')
    expect(signup.emitted().signup[0]).toEqual(['valid_username'])
  })

ref

我们有时需要访问 DOM 元素或子组件,以手动操作它们,而不是依赖数据绑定,例如一个 进入页面之后,自动聚焦的输入框,如何使用 ref 自动聚焦?

  <script setup lang="ts">
  import { onMounted, ref } from 'vue'
  const input = ref<HTMLInputElement | null>(null)
  const model = defineModel<string>()
  onMounted(() => {
    input.value?.focus()
  })
</script>
  <template>
    <div>
      <input ref="input" v-model="model" />
    </div>
  </template>

我们要测两点东西:

1. ref 获取 input 元素存在

2. input 元素被聚焦了

  import { shallowMount } from '@vue/test-utils'
  import Ref from './Ref.vue'
  describe('Ref', () => {
    it('自动聚焦的输入框', () => {
      const wrapper = shallowMount(Ref, {
        attachTo: document.body
      })
      const input = wrapper.find<HTMLInputElement>({
        ref: 'input'
      })
      expect(document.activeElement).toBe(input.element)
    })
  })

defineAsyncComponent

异步加载组件的加载可能还比较少人用过,具体可以看看,

看一个异步加载到的 demo , Lazy.vue 组件内部有一个异步的 pdf 预览组件.

  // Lazy.vue
  <script setup lang="ts">
  import { defineAsyncComponent } from 'vue'
  // 简单用法
  const AsyncPdf = defineAsyncComponent({
    loader: () => import('./AsyncPdf.vue'),
    delay: 200,
  })
</script>
  <template>
    <h4 style="color: red">测试 async</h4>
    <br />
    <AsyncPdf></AsyncPdf>
  </template>
  // AsyncPdf.vue
  <script setup lang="ts">
</script>
  <template>
    <div>
      <div>pdf file</div>
      <div v-if="false" data-testid="if">if button</div>
      <div v-show="false" data-testid="show">show button</div>
    </div>
  </template>

我们只需要测试,1秒之后,页面出现 pdf。

  import { mount } from '@vue/test-utils'
  import Lazy from './Lazy.vue'
  describe('Lazy', () => {
    it('renders Lazy component', async () => {
      const wrapper = mount(Lazy)
      expect(wrapper.text()).not.toContain('pdf')
      await new Promise((resolve) => setTimeout(resolve, 1000))
      expect(wrapper.text()).toContain('pdf')
    })
  })

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

在这里插入图片描述

在这里插入图片描述

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值