Vue测试策略踩坑

28.1 Vue单元测试的常见错误

核心知识点

  • Vue 单元测试用于测试组件、工具函数等独立单元
  • 常见错误包括:测试范围过大、未模拟依赖、断言不当等
  • 正确的单元测试需要确保测试范围小、模拟依赖、断言准确

实用案例分析

错误场景

// 错误测试:测试范围过大
// UserComponent.test.js
import { mount } from '@vue/test-utils'
import UserComponent from '@/components/UserComponent.vue'
import axios from 'axios'

// 错误:测试了网络请求,超出单元测试范围
test('should fetch user data', async () => {
  const wrapper = mount(UserComponent)
  await wrapper.find('button').trigger('click')
  expect(wrapper.text()).toContain('User: John')
})

正确实现

// 正确测试:单元测试范围
// UserComponent.test.js
import { shallowMount } from '@vue/test-utils'
import UserComponent from '@/components/UserComponent.vue'
import axios from 'axios'

// 模拟 axios
jest.mock('axios', () => ({
  get: jest.fn().mockResolvedValue({
    data: { name: 'John' }
  })
}))

test('should render user component', () => {
  const wrapper = shallowMount(UserComponent)
  expect(wrapper.exists()).toBe(true)
})

test('should call fetchUser when button is clicked', async () => {
  const wrapper = shallowMount(UserComponent)
  const fetchUserSpy = jest.spyOn(wrapper.vm, 'fetchUser')
  await wrapper.find('button').trigger('click')
  expect(fetchUserSpy).toHaveBeenCalled()
})

test('should update user data when fetchUser is called', async () => {
  const wrapper = shallowMount(UserComponent)
  await wrapper.vm.fetchUser()
  expect(wrapper.vm.user.name).toBe('John')
})

28.2 Vue组件测试的陷阱

核心知识点

  • Vue 组件测试用于测试组件的渲染、交互等
  • 常见陷阱包括:未模拟子组件、交互测试不当、断言不准确等
  • 正确的组件测试需要确保模拟子组件、测试真实交互、断言准确

实用案例分析

错误场景

// 错误测试:未模拟子组件
// ParentComponent.test.js
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent.vue'
import ChildComponent from '@/components/ChildComponent.vue' // 未模拟

test('should render parent component with child', () => {
  const wrapper = mount(ParentComponent) // 会渲染真实的 ChildComponent
  expect(wrapper.find('child-component').exists()).toBe(true)
})

正确实现

// 正确测试:模拟子组件
// ParentComponent.test.js
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent.vue'

// 模拟子组件
const mockChildComponent = {
  name: 'ChildComponent',
  template: '<div>Mock Child</div>'
}

test('should render parent component with child', () => {
  const wrapper = mount(ParentComponent, {
    components: {
      ChildComponent: mockChildComponent
    }
  })
  expect(wrapper.exists()).toBe(true)
  expect(wrapper.text()).toContain('Mock Child')
})

test('should pass props to child component', () => {
  const wrapper = mount(ParentComponent, {
    components: {
      ChildComponent: {
        name: 'ChildComponent',
        props: ['title'],
        template: '<div>{{ title }}</div>'
      }
    }
  })
  expect(wrapper.text()).toContain('Parent Title')
})

28.3 Vue集成测试的使用误区

核心知识点

  • Vue 集成测试用于测试多个组件或模块的协作
  • 常见误区包括:测试过于复杂、未模拟外部依赖、测试速度慢等
  • 正确的集成测试需要确保测试目标明确、模拟外部依赖、保持测试速度

实用案例分析

错误场景

// 错误测试:测试过于复杂
// App.test.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'
import router from '@/router'
import store from '@/store'

// 错误:测试了整个应用,过于复杂
test('should render full app', async () => {
  const wrapper = mount(App, {
    global: {
      plugins: [router, store]
    }
  })
  await router.push('/')
  expect(wrapper.text()).toContain('Home Page')
  await router.push('/about')
  expect(wrapper.text()).toContain('About Page')
})

正确实现

// 正确测试:集成测试目标明确
// UserFlow.test.js
import { mount } from '@vue/test-utils'
import UserList from '@/components/UserList.vue'
import UserDetail from '@/components/UserDetail.vue'
import { createStore } from 'vuex'

// 创建测试 store
const createTestStore = () => {
  return createStore({
    state: {
      users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
    },
    getters: {
      allUsers: state => state.users
    }
  })
}

test('should display user list and detail', async () => {
  const store = createTestStore()
  const wrapper = mount(UserList, {
    global: {
      plugins: [store]
    }
  })
  
  // 测试用户列表渲染
  expect(wrapper.findAll('.user-item')).toHaveLength(2)
  
  // 测试点击用户显示详情
  await wrapper.find('.user-item').trigger('click')
  const detailWrapper = mount(UserDetail, {
    props: {
      userId: 1
    },
    global: {
      plugins: [store]
    }
  })
  expect(detailWrapper.text()).toContain('User: John')
})

28.4 Vue端到端测试的常见问题

核心知识点

  • Vue 端到端测试用于测试完整的用户流程
  • 常见问题包括:测试不稳定、速度慢、断言不当等
  • 正确的端到端测试需要确保测试稳定、速度合理、断言准确

实用案例分析

错误场景

// 错误测试:测试不稳定
// cypress/e2e/user-flow.cy.js
// 错误:使用固定等待时间,不稳定
it('should login and view dashboard', () => {
  cy.visit('/login')
  cy.get('input[name="username"]').type('admin')
  cy.get('input[name="password"]').type('password')
  cy.get('button[type="submit"]').click()
  cy.wait(2000) // 错误:固定等待时间
  cy.visit('/dashboard')
  cy.contains('Dashboard').should('be.visible')
})

正确实现

// 正确测试:端到端测试稳定
// cypress/e2e/user-flow.cy.js
it('should login and view dashboard', () => {
  cy.visit('/login')
  cy.get('input[name="username"]').type('admin')
  cy.get('input[name="password"]').type('password')
  cy.get('button[type="submit"]').click()
  // 正确:使用条件等待
  cy.url().should('include', '/dashboard')
  cy.contains('Dashboard').should('be.visible')
})

it('should create a new user', () => {
  cy.login() // 使用自定义命令
  cy.visit('/users')
  cy.get('button[data-testid="add-user"]').click()
  cy.get('input[name="name"]').type('New User')
  cy.get('button[type="submit"]').click()
  cy.contains('New User').should('be.visible')
})

// cypress/support/commands.js
Cypress.Commands.add('login', () => {
  cy.visit('/login')
  cy.get('input[name="username"]').type('admin')
  cy.get('input[name="password"]').type('password')
  cy.get('button[type="submit"]').click()
  cy.url().should('include', '/dashboard')
})

28.5 Vue测试覆盖率的陷阱

核心知识点

  • Vue 测试覆盖率用于衡量测试的覆盖程度
  • 常见陷阱包括:追求 100% 覆盖率、覆盖率数据误导、未分析覆盖率报告等
  • 正确的测试覆盖率需要确保覆盖关键路径、分析覆盖率报告、关注质量而非数量

实用案例分析

错误场景

// 错误测试:追求 100% 覆盖率
// utils.test.js
import { formatDate } from '@/utils/date'

// 错误:测试了所有分支,包括不可能的情况
test('should format date with different formats', () => {
  expect(formatDate(new Date(), 'YYYY-MM-DD')).toBeTruthy()
  expect(formatDate(new Date(), 'MM/DD/YYYY')).toBeTruthy()
  expect(formatDate(null, 'YYYY-MM-DD')).toBe('') // 测试了 null 输入
  expect(formatDate(undefined, 'YYYY-MM-DD')).toBe('') // 测试了 undefined 输入
})

正确实现

// 正确测试:关注关键路径
// utils.test.js
import { formatDate } from '@/utils/date'

test('should format date with YYYY-MM-DD format', () => {
  const date = new Date('2023-01-01')
  expect(formatDate(date, 'YYYY-MM-DD')).toBe('2023-01-01')
})

test('should format date with MM/DD/YYYY format', () => {
  const date = new Date('2023-01-01')
  expect(formatDate(date, 'MM/DD/YYYY')).toBe('01/01/2023')
})

test('should handle invalid date', () => {
  expect(formatDate(null, 'YYYY-MM-DD')).toBe('')
})

// 分析覆盖率报告
// 运行:npm test -- --coverage
// 查看 coverage/lcov-report/index.html
// 关注未覆盖的关键路径

28.6 Vue测试工具配置的使用误区

核心知识点

  • Vue 测试工具配置包括 Jest、Cypress、Vue Test Utils 等
  • 常见误区包括:配置错误、版本不兼容、插件冲突等
  • 正确的配置需要确保版本兼容、配置正确、避免冲突

实用案例分析

错误场景

// 错误配置:测试工具版本不兼容
// package.json
{
  "devDependencies": {
    "jest": "^29.0.0",
    "@vue/test-utils": "^1.3.0", // 错误:@vue/test-utils 1.x 不兼容 Vue 3
    "vue-jest": "^4.0.0" // 错误:vue-jest 4.x 不兼容 Jest 29
  }
}

正确实现

// 正确配置:测试工具版本兼容
// package.json (Vue 2)
{
  "devDependencies": {
    "jest": "^26.0.0",
    "@vue/test-utils": "^1.3.0",
    "vue-jest": "^3.0.7",
    "babel-jest": "^26.0.0"
  }
}

// package.json (Vue 3)
{
  "devDependencies": {
    "jest": "^29.0.0",
    "@vue/test-utils": "^2.0.0",
    "@vue/vue3-jest": "^29.0.0",
    "babel-jest": "^29.0.0"
  }
}

// jest.config.js
module.exports = {
  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
  transform: {
    '^.+\\.vue$': '@vue/vue3-jest',
    '^.+\\.js$': 'babel-jest'
  },
  testMatch: ['**/*.test.js'],
  collectCoverageFrom: ['src/**/*.{js,vue}'],
  coverageDirectory: '<rootDir>/coverage'
}

28.7 Vue模拟数据的常见错误

核心知识点

  • Vue 模拟数据用于测试中替代真实数据
  • 常见错误包括:模拟数据不真实、未覆盖边界情况、模拟过于复杂等
  • 正确的模拟数据需要确保数据真实、覆盖边界情况、模拟简洁

实用案例分析

错误场景

// 错误模拟:模拟数据不真实
// userService.test.js
import userService from '@/services/userService'
import axios from 'axios'

jest.mock('axios', () => ({
  get: jest.fn().mockResolvedValue({
    data: {} // 错误:空对象,不真实
  })
}))

test('should fetch user', async () => {
  const user = await userService.fetchUser(1)
  expect(user).toBeDefined()
})

正确实现

// 正确模拟:模拟数据真实
// userService.test.js
import userService from '@/services/userService'
import axios from 'axios'

// 真实的模拟数据
const mockUser = {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
  role: 'user'
}

jest.mock('axios', () => ({
  get: jest.fn().mockResolvedValue({
    data: mockUser
  })
}))

test('should fetch user', async () => {
  const user = await userService.fetchUser(1)
  expect(user).toEqual(mockUser)
  expect(user.id).toBe(1)
  expect(user.name).toBe('John Doe')
})

// 测试边界情况
test('should handle 404 error', async () => {
  axios.get.mockRejectedValue({
    response: {
      status: 404
    }
  })
  await expect(userService.fetchUser(999)).rejects.toThrow('User not found')
})

28.8 Vue测试环境的陷阱

核心知识点

  • Vue 测试环境包括测试数据库、环境变量、浏览器环境等
  • 常见陷阱包括:环境配置错误、数据库未隔离、环境变量冲突等
  • 正确的测试环境需要确保配置正确、环境隔离、变量合理

实用案例分析

错误场景

// 错误环境:测试环境配置错误
// .env.test
NODE_ENV=production // 错误:测试环境应为 test
VUE_APP_API_BASE_URL=https://api.example.com // 错误:应使用测试 API

// 测试数据库未隔离
// 使用了开发数据库,可能影响开发数据

正确实现

// 正确环境:测试环境配置
// .env.test
NODE_ENV=test
VUE_APP_API_BASE_URL=http://localhost:3000/api // 测试 API
VUE_APP_DB_NAME=test_db // 测试数据库

// jest.config.js 中加载环境变量
require('dotenv').config({ path: '.env.test' })

// 测试数据库隔离
// 使用内存数据库或测试专用数据库
// userService.test.js
import userService from '@/services/userService'
import { setupTestDB, teardownTestDB } from '@/tests/utils'

beforeEach(async () => {
  await setupTestDB() // 重置测试数据库
})

afterEach(async () => {
  await teardownTestDB() // 清理测试数据库
})

test('should create user', async () => {
  const user = await userService.createUser({ name: 'Test User' })
  expect(user.id).toBeDefined()
  expect(user.name).toBe('Test User')
})
« 上一篇 Vue版本迁移踩坑 下一篇 » Vue性能优化策略踩坑