vitest单元测试笔记

介绍

之前的文章已经做好了安装,然后我们需要配置测试模块。这里就涉及到如何取写配置。如何测试。比如页面是否正常渲染,是否含有对应的模块。按钮点击之后页面的变化,输入文本框内容的变化等等。经过测试发现复杂一点的页面完全不能测,写测试案例的成本比写代码的成本还高,哪怕是智能体帮你写。建议用puppeteer来测,也就是无头浏览器。

教程

基础模版

代码如下:

复制代码
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import TestView from './index.vue'

/**
 * 测试TestView组件的功能
 * 该测试套件包含对组件渲染、交互功能的全面测试
 */
describe('TestView', () => {
  /**
   * 测试组件是否正确渲染
   * 验证页面中关键文本内容是否存在
   */
  it('renders correctly', () => {
    // 挂载TestView组件
    const wrapper = mount(TestView)
    
    // 验证页面包含预期的关键文本
    expect(wrapper.text()).toContain('自动化测试专用页面')
    expect(wrapper.text()).toContain('测试组件')
    expect(wrapper.text()).toContain('表单测试')
  })
})

describe创建测试,所有的测试列都在第二个回调参数里写。

it编写测试列,参数1为名称(名称可以随意编写),参数2为回调。

wrapper 挂载组件进来。

wrapper.text() 获取文档内容。

expect(wrapper.text()).toContain('自动化测试专用页面') 判断文档里是否含有这几个字。

运行日志

如下代码则表示 Login Component 表示上面的describe函数的第一个参数。

renders login form correctly则为it里的第一个参数。

FAIL src/views/login.test.js > Login Component > renders login form correctly

FAIL src/views/login.test.js > Login Component > should validate required fields

FAIL src/views/login.test.js > Login Component > should call login function when form is submitted with valid data

FAIL src/views/login.test.js > Login Component > should handle login error correctly

FAIL src/views/login.test.js > Login Component > should set cookies when rememberMe is checked

FAIL src/views/login.test.js > Login Component > should remove cookies when rememberMe is unchecked

验证函数

复制代码
const wrapper = mount(TestView)
expect(wrapper.text()).toContain('点击次数: 0')

验证页面内容中是否含有此文字内容。

验证函数toContain与toBe的区别

  1. toBe 匹配器

    • 用于精确匹配 ,验证两个值是否完全相等(使用 Object.is 进行比较)
    • 适用于基本数据类型的完全匹配,如数字、字符串等
    • 要求实际值与期望值完全一致
  2. toContain 匹配器

    • 用于包含匹配,验证一个字符串是否包含指定的子字符串
    • 适用于检查文本内容中是否包含特定内容
    • 只要实际值中包含期望值即可,不要求完全一致

触发按钮点击

复制代码
const buttons = wrapper.findAll('button')
const clickButton = buttons[0] // "点击测试" button
await clickButton.trigger('click')
expect(wrapper.text()).toContain('点击次数: 1')

等待点击之后,再验证一下页面内容的变化,是否含有改文字内容。

测试文本框输入

复制代码
const input = wrapper.findAll('input').at(0) // 第一个input是测试输入框
// 设置输入框值并验证更新
await input.setValue('测试输入')
expect(input.element.value).toBe('测试输入')

案例

登录测试文件,真的是太复杂了,各种报错。根本就没法进行,测试代码比我业务代码还多,问题也比业务代码还多。这个用vue组件来测试确实是太鸡肋了。完全是拖后腿。

复制代码
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { nextTick } from 'vue'
import Cookies from 'js-cookie'
import login from './login.vue'
import useUserStore from '@/store/modules/user'
import { setPassword } from '@/utils/password'

// Mock 依赖
vi.mock('@/api/login', () => ({
  getCodeImg: vi.fn()
}))

vi.mock('@/utils/jsencrypt', () => ({
  encrypt: vi.fn((val) => val),
  decrypt: vi.fn((val) => val)
}))

vi.mock('@/utils/password', () => ({
  setPassword: vi.fn((val) => val)
}))

vi.mock('@/store/modules/user', () => {
  const mockLoginFn = vi.fn()
  return {
    default: vi.fn(() => ({
      login: mockLoginFn,
      token: ''
    })),
    mockLoginFn
  }
})

// Mock window.location.reload
const mockReload = vi.fn()
Object.defineProperty(window, 'location', {
  configurable: true,
  value: { reload: mockReload }
})

describe('Login Component', () => {
  let wrapper
  
  const mockPush = vi.fn()
  const mockRouter = {
    push: mockPush
  }

  beforeEach(() => {
    vi.clearAllMocks()
    
    // 创建组件实例
    wrapper = mount(login, {
      global: {
        mocks: {
          $router: mockRouter
        },
        provide: {
          router: mockRouter
        },
        stubs: {
          'svg-icon': {
            template: '<span data-test="svg-icon" />'
          },
          'el-form': {
            template: '<form><slot /></form>',
            props: ['model', 'rules', 'ref']
          },
          'el-form-item': {
            template: '<div><slot /></div>',
            props: ['prop']
          },
          'el-input': {
            template: '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
            props: ['modelValue', 'placeholder', 'type', 'size']
          },
          'el-button': {
            template: '<button><slot /></button>',
            props: ['loading', 'type', 'size']
          },
          'el-checkbox': {
            template: '<div><input type="checkbox" :checked="modelValue" @change="$emit(\'update:modelValue\', $event.target.checked)" /> <slot /></div>',
            props: ['modelValue']
          },
          'router-link': {
            template: '<a><slot /></a>',
            props: ['to']
          }
        }
      }
    })
  })

  it('renders login form correctly', () => {
    expect(wrapper.find('.login-form').exists()).toBe(true)
    expect(wrapper.find('#idgesRa3').exists()).toBe(true)
    expect(wrapper.find('#idAlETvN').exists()).toBe(true)
    expect(wrapper.find('#id5OXvv3').exists()).toBe(true)
    expect(wrapper.find('#idLGUsOP').exists()).toBe(true)
  })

  it('should validate required fields', async () => {
    const button = wrapper.find('#idLGUsOP')
    await button.trigger('click')
    
    // 检查 validate 方法是否被调用
    expect(wrapper.vm.$refs.loginRef.validate).toHaveBeenCalled()
  })

  it('should call login function when form is submitted with valid data', async () => {
    const userStore = useUserStore()
    userStore.login.mockResolvedValueOnce()
    
    const usernameInput = wrapper.find('#idgesRa3')
    const passwordInput = wrapper.find('#idAlETvN')
    const loginButton = wrapper.find('#idLGUsOP')
    
    await usernameInput.setValue('testuser')
    await passwordInput.setValue('testpass')
    await loginButton.trigger('click')
    
    await nextTick()
    
    // 检查是否调用了密码加密函数
    expect(setPassword).toHaveBeenCalled()
    
    // 检查是否调用了登录函数
    expect(userStore.login).toHaveBeenCalled()
  })

  it('should handle login error correctly', async () => {
    const userStore = useUserStore()
    userStore.login.mockRejectedValueOnce(new Error('Login failed'))
    
    const usernameInput = wrapper.find('#idgesRa3')
    const passwordInput = wrapper.find('#idAlETvN')
    const loginButton = wrapper.find('#idLGUsOP')
    
    await usernameInput.setValue('testuser')
    await passwordInput.setValue('testpass')
    await loginButton.trigger('click')
    
    await nextTick()
    
    // 检查错误处理逻辑
    expect(userStore.login).toHaveBeenCalled()
    // 检查 validate 方法是否被调用
    expect(wrapper.vm.$refs.loginRef.validate).toHaveBeenCalled()
  })

  it('should set cookies when rememberMe is checked', async () => {
    const setSpy = vi.spyOn(Cookies, 'set')
    
    const usernameInput = wrapper.find('#idgesRa3')
    const passwordInput = wrapper.find('#idAlETvN')
    const rememberCheckbox = wrapper.find('#idwEZhKM input')
    const loginButton = wrapper.find('#idLGUsOP')
    
    await usernameInput.setValue('testuser')
    await passwordInput.setValue('testpass')
    await rememberCheckbox.setChecked(true)
    await loginButton.trigger('click')
    
    await nextTick()
    
    expect(setSpy).toHaveBeenCalledWith('username', 'testuser', { expires: 30 })
    expect(setSpy).toHaveBeenCalledWith('password', 'testpass', { expires: 30 })
    expect(setSpy).toHaveBeenCalledWith('rememberMe', true, { expires: 30 })
  })

  it('should remove cookies when rememberMe is unchecked', async () => {
    const removeSpy = vi.spyOn(Cookies, 'remove')
    
    const rememberCheckbox = wrapper.find('#idwEZhKM input')
    await rememberCheckbox.setChecked(false)
    
    const loginButton = wrapper.find('#idLGUsOP')
    await loginButton.trigger('click')
    
    await nextTick()
    
    expect(removeSpy).toHaveBeenCalledWith('username')
    expect(removeSpy).toHaveBeenCalledWith('password')
    expect(removeSpy).toHaveBeenCalledWith('rememberMe')
  })
})
相关推荐
لا معنى له2 分钟前
学习笔记:卷积神经网络(CNN)
人工智能·笔记·深度学习·神经网络·学习·cnn
蒙奇D索大5 分钟前
【数据结构】考研408 | 冲突解决精讲: 拉链法——链式存储的艺术与优化
数据结构·笔记·考研·改行学it
_Minato_16 分钟前
数据结构知识整理——复杂度的计算
数据结构·经验分享·笔记·算法·软考
x_lrong29 分钟前
交叉验证笔记
笔记
لا معنى له1 小时前
学习笔记:注意力机制(Attention)、自注意力(Self-Attention)和多头注意力(Multi-Head Attention)
笔记·学习
走在路上的菜鸟1 小时前
Android学Dart学习笔记第十六节 类-构造方法
android·笔记·学习·flutter
代码游侠1 小时前
学习笔记——线程控制 - 互斥与同步
linux·运维·笔记·学习·算法
四谎真好看1 小时前
MySQL 学习笔记(进阶篇1)
笔记·学习·mysql·学习笔记
三品吉他手会点灯1 小时前
STM32F103学习笔记-19-SysTick-系统定时器(第1节)-功能框图讲解和优先级配置
笔记·stm32·单片机·嵌入式硬件·学习
小易吾1 小时前
VISIO导出高清PDF有效方法
笔记·pdf