Vue3引入Vitest和Vue Test Utils

412 阅读2分钟

初始配置

使用到的依赖如下

"@vitest/ui": "^1.6.0",
"@vue/test-utils": "^2.4.6",
"jsdom": "^24.0.0",
"vitest": "^1.6.0",
"flush-promises": "^1.0.2",

tsconfig.json中配置

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "Node",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "jsxImportSource": "vue",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "paths": {
      "@/*": ["./src/*"],
      "_c/*": ["./src/components"]
    },
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue","tests/**/*.ts","tests/**/*.tsx"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

新增vitest.config.js配置

import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      environment: 'jsdom',
      exclude: [...configDefaults.exclude, 'e2e/**'],
      root: fileURLToPath(new URL('./', import.meta.url)
      )
    }
  })
)

实践一下

新增两个组件 HelloWorld.vue

<template>
  <h1>{{msg}}</h1>
  <button @click="setCount">{{count}}</button>
  <input type="text" v-model="todo"/>
  <button class="addTodo" @click="addTodo">add</button>
  <button class="loadUser" @click="loadUser">load</button>
  <p v-if="user.loading" class="loading">Loading</p>
  <div v-else class="userName">{{user.data && user.data.username}}</div>
  <p v-if="user.error" class="error">error!</p>
  <ul>
    <li v-for="(todo, index) in todos" :key="index">{{todo}}</li>
  </ul>
  <hello msg="1234"></hello>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import Hello from './Hello.vue'
import axios from 'axios'
export default defineComponent({
  name: 'HelloWorld',
  components: {
    Hello
  },
  props: {
    msg: String
  },
  emits: ['send'],
  setup(props, context) {
    const todo = ref('')
    const todos = ref<string[]>([])
    const user = reactive({
      data: null as any,
      loading: false,
      error: false
    })
    const count = ref(1)
    const setCount = () => {
      count.value++
    }
    const addTodo = () => {
      if (todo.value) {
        todos.value.push(todo.value)
        context.emit('send', todo.value)
      }
    }
    const loadUser = () => {
      user.loading = true
      axios.get('https://jsonplaceholder.typicode.com/users/1').then(resp => {
        console.log(resp)
        user.data = resp.data
      }).catch(() => {
        user.error = true
      }).finally(() => {
        user.loading = false
      })
    }
    return {
      count,
      setCount,
      todo,
      todos,
      addTodo,
      user,
      loadUser,
    }
  }
})
</script>

Hello.vue

<template>
  <h1 class="hello">{{msg}}</h1>
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'Hello',
  props: {
    msg: String
  }
})
</script>

新建测试文件tests/unit/example.spec.ts

import { shallowMount, VueWrapper } from '@vue/test-utils'
import axios from 'axios'
import flushPromises from 'flush-promises'
import HelloWorld from '@/components/HelloWorld.vue'
import Hello from '@/components/Hello.vue'
import {vi,describe,it,beforeAll,afterEach,expect} from "vitest"
vi.mock('axios')
// 强制类型转换,以确保 mock 后的 Axios 保持类型信息
const mockAxiosGet = vi.spyOn(axios, 'get')

const msg = 'new message'
let wrapper: VueWrapper<any>
describe('HelloWorld.vue', () => {
    beforeAll(() => {
        wrapper = shallowMount(HelloWorld, {
            props: { msg }
        })
    })
    it('renders props.msg when passed', () => {
        expect(wrapper.get('h1').text()).toBe(msg)
        expect(wrapper.findComponent(Hello).exists()).toBeTruthy()
    })
    it('should update the count when clicking the button', async () => {
        await wrapper.get('button').trigger('click')
        expect(wrapper.get('button').text()).toBe('2')
    })
    it('should add todo when fill the input and click the add button', async () => {
        const todoContent = 'buy milk'
        await wrapper.get('input').setValue(todoContent)
        expect(wrapper.get('input').element.value).toBe(todoContent)
        await wrapper.get('.addTodo').trigger('click')
        expect(wrapper.findAll('li')).toHaveLength(1)
        expect(wrapper.get('li').text()).toBe(todoContent)
        console.log(wrapper.emitted())
        expect(wrapper.emitted()).toHaveProperty('send')
        const events = wrapper.emitted('send')
        expect(events && events[0]).toEqual([todoContent])
    })
    it('should load user message when click the load button', async () => {
        mockAxiosGet.mockResolvedValueOnce({ data: { username: 'viking'}})
        await wrapper.get('.loadUser').trigger('click')
        expect(mockAxiosGet).toHaveBeenCalled()
        expect(wrapper.find('.loading').exists()).toBeTruthy()
        await flushPromises()
        // 界面更新完毕
        console.log(1111,wrapper.find('.userName').text())
        expect(wrapper.find('.loading').exists()).toBeFalsy()
        expect(wrapper.get('.userName').text()).toBe('viking')
    })
    it('should load error when return promise reject', async () => {
        mockAxiosGet.mockRejectedValueOnce('error')
        await wrapper.get('.loadUser').trigger('click')
        expect(mockAxiosGet).toHaveBeenCalledTimes(1)
        await flushPromises()
        expect(wrapper.find('.loading').exists()).toBe(false)
        expect(wrapper.find('.error').exists()).toBe(true)
    })
    afterEach(() => {
        mockAxiosGet.mockReset()
    })
})

在package.json中配置 或者在控制台直接运行vitest

  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "test:unit": "vitest"
  },