vitest 单元测试配合@vue/test-utils 之 pinia 篇

文章介绍了Vitest作为Vue.js的极速单元测试框架,以及VueTestUtils作为官方测试工具库如何用于组件测试。同时,文章详细展示了如何在测试中使用Pinia进行状态管理,包括在组件和store中的测试用例。通过示例代码,解释了如何激活Pinia、创建断言以及模拟用户交互。文章还探讨了在测试环境中设置和使用Pinia的不同方法。

what is vitest & test-utils & pinia

vitest 是由 vite 提供支持的极速单元测试框架,VueTestUtils 是 Vue.js 的官方测试实用程序库,pinia 是 Vue.js 的状态管理库,以上均为各自官网对其的描述

demo

项目中使用状态管理是非常常见的,所以对它也可以来个单元测试,这里我们可能会有两个场景:1. 测试在组件中使用 pinia;2. 直接测试 store

// store
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(1)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    return count.value++
  }

  return { count, doubleCount, increment }
})
// component
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
function increment() {
  store.increment()
}
</script>

<template>
  <div>count: {{ store.count }}</div>
  <button @click="increment">increment</button>
</template>
// spec | test
import { describe, test, expect, beforeEach } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import HomeView from '@/views/HomeView.vue'

describe('demo1', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  test('component-pinia', async () => {
    const wrapper = shallowMount(HomeView)
    expect(wrapper.html()).toContain('count: 1')
    await wrapper.find('button').trigger('click')
    expect(wrapper.html()).toContain('count: 2')
  })
})
  1. 第一个 demo 由三个部分组成,store、组件和测试文件
  2. store 里是一个常见的案例,使用了 SetupStore 的写法,与 OptionStore 相比,ref()就是state,computed()就是getters,function()就是actions,定义了一个 count,一个 count 自增的函数和计算属性双倍获取 count 值
  3. 组件的逻辑非常简单,点击按钮,增加 store 里的 count 值,页面显示实时数字
  4. 测试文件中因为是在单元测试环境下,所以需要在测试代码运行之前即beforeEach中,显式激活 piniasetActivePinia(createPinia())

    使用 test-utils 库的 shallowMount 方法浅渲染挂载组件
    组件通过 html 方法返回元素的 HTML
    然后使用 vitest 库的 expect 创建断言,toContain 是断言检查值是否在数组中
    组件通过 find 方法返回查找元素,通过 trigger 方法触发 DOM 事件,模拟用户按下按钮的操作,按下按钮前后都断言了,会判断实际执行结果与预期结果是否一致,如果不一致则会抛出错误

第二个 demo 仍使用相同的 store 文件,只需要重新写一个测试文件

import { describe, test, expect, beforeAll } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useCounterStore } from '@/store/counter'

describe('demo2', () => {
  let store: any
  beforeAll(() => {
    setActivePinia(createPinia())
    store = useCounterStore()
  })
  test('count', () => {
    const { count } = store
    expect(count).toBe(1)
  })
  test('doubleCount', () => {
    const { doubleCount } = store
    expect(doubleCount).toBe(2)
  })
})

第二个测试 demo 仍是在测试代码运行前显示激活 pinia
用了两个测试组分别测试了从 store 中获取 count 和计算双倍 count 是否与预期值一致

tips

虽然这仍是一个简单的 demo,但它还是有很多值得注意的知识点的

  • 测试环境:测试环境里是没有 pinia 的,所以我们需要在测试代码运行之前激活 pinia,所以用到了beforeEachsetActivePinia,这里使用beforeAll也可以,它是在所有测试代码运行之前调用
  • 在 test-utils 的文档里有 vuex 的测试案列,需要在 mount(Component, { global: { plugins: [store] } })中用挂载选项安装插件,但 pinia 这边测试挂不挂载插件都是一样的,不会报错,测试也是正常运行,不知道这是不是 pinia 的特性,有知道的可以评论告知,但是在 pinia 文档中组件单元测试的案例是使用@pinia/testing,它又是在挂载选项中安装了插件的,所以不知道我的操作是不是正确的
// test-utils文档通过挂载选项安装插件
import { createStore } from 'vuex'

const store = createStore()
const wrapper = mount(Component, {
  global: {
    plugins: [store],
  },
})
// pinia文档中的案例是
import { createTestingPinia } from '@pinia/testing'

const wrapper = mount(Counter, {
  global: {
    plugins: [createTestingPinia()],
  },
})

其他文章

vitest 单元测试配合@vue/test-utils 之组件单元测试篇

vue-typescript-admin-template@0.1.0 D:\project-sky-admin-vue-ts3 ├─┬ @types/vue-router@2.0.0 │ └── vue-router@3.6.5 deduped ├─┬ @vue/cli-plugin-babel@3.12.1 │ └─┬ @vue/babel-preset-app@3.12.1 │ └─┬ @vue/babel-preset-jsx@1.4.0 │ └── vue@2.7.16 deduped ├─┬ @vue/cli-plugin-typescript@3.12.1 │ ├─┬ fork-ts-checker-webpack-plugin@0.5.2 │ │ └── typescript@3.6.2 deduped │ ├─┬ ts-loader@5.4.5 │ │ └── typescript@3.6.2 deduped │ ├─┬ tslint@5.20.1 │ │ ├─┬ tsutils@2.29.0 │ │ │ └── typescript@3.6.2 deduped │ │ └── typescript@3.6.2 deduped │ └── typescript@3.6.2 deduped ├─┬ @vue/cli-plugin-unit-jest@3.12.1 │ └─┬ vue-jest@3.0.7 │ └── vue@2.7.16 deduped ├─┬ @vue/eslint-config-typescript@4.0.0 │ └─┬ @typescript-eslint/eslint-plugin@1.13.0 │ └─┬ tsutils@3.21.0 │ └── typescript@3.6.2 deduped ├─┬ @vue/test-utils@1.3.6 │ └── vue@2.7.16 deduped ├─┬ element-ui@2.15.14 │ └── vue@2.7.16 deduped ├── typescript@3.6.2 ├─┬ vue-area-linkage@5.1.0 │ └── vue@2.7.16 deduped ├─┬ vue-class-component@7.2.6 │ └── vue@2.7.16 deduped ├─┬ vue-property-decorator@8.5.1 │ └── vue@2.7.16 deduped ├── vue-router@3.6.5 ├─┬ vue-svgicon@3.3.2 │ └── vue@2.7.16 deduped ├── vue@2.7.16 ├─┬ vuex-class@0.3.2 │ └── vue@2.7.16 deduped ├─┬ vuex-module-decorators@0.10.1 │ └── vue@2.7.16 deduped ├─┬ vuex-persistedstate@2.7.1 │ └── vue@2.7.16 deduped └─┬ vuex@3.6.2 └── vue@2.7.16 deduped PS D:\project-sky-admin-vue-ts3>
08-01
请你阅读我的vue项目的package.json文件,你认为该项目集成electron最好使用哪个版本比较稳定且好下载呢? { "name": "vms", "version": "1.7.8", "private": true, "scripts": { "serve": "cross-env vue-cli-service serve --type=local --model=single --access=normal", "serve:software": "cross-env vue-cli-service serve --type=local --model=single --access=normal --env=software", "serve:mock": "cross-env vue-cli-service serve --type=local --model=single --access=normal --env=mock", "serve:cloud": "cross-env vue-cli-service serve --type=cloud --model=single --access=normal", "build": "cross-env vue-cli-service build --type=local --model=single --access=normal --env=prd", "build:cloud": "cross-env vue-cli-service build --type=cloud --model=single --access=normal", "build:prd": "cross-env vue-cli-service build --type=cloud --model=single --access=normal --env=prd", "test:unit": "vue-cli-service test:unit", "prepare": "husky install", "lint": "vue-cli-service lint", "lint:staged": "lint-staged -c ./.husky/lint-staged.config.js", "sonar": "sonar-scanner" }, "dependencies": { "@antv/g2plot": "^2.4.31", "@fe/arch-player": "1.4.31", "@fe/canvas-timeline": "4.0.0-alpha-0.0.37", "@mapbox/mapbox-gl-geocoder": "^5.0.2", "@mapbox/mapbox-sdk": "^0.15.1", "@paypal/paypal-js": "^5.1.1", "@stripe/stripe-js": "^1.18.0", "@turf/bbox": "^7.0.0", "@vueuse/core": "^9.1.0", "ant-design-vue": "^3.2.13", "await-to-js": "^3.0.0", "axios": "^1.4.0", "browser-image-compression": "^2.0.2", "copy-to-clipboard": "^3.3.1", "core-js": "^3.6.5", "crypto-js": "^4.1.1", "docx": "^9.1.1", "echarts": "^5.3.2", "exceljs": "^4.4.0", "file-saver": "^2.0.5", "html2pdf.js": "^0.10.2", "js-base64": "^3.7.5", "js-md5": "^0.7.3", "jsencrypt": "^3.3.0", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.4", "jszip": "^3.10.1", "konva": "^8.3.10", "mapbox-gl": "^2.15.0", "mitt": "^3.0.0", "moment": "^2.29.1", "moment-timezone": "^0.5.34", "node-polyfill-webpack-plugin": "^2.0.1", "node-rsa": "^1.1.1", "ol": "^7.3.0", "pdf-merger-js": "^5.1.2", "pdfobject": "^2.2.6", "pinia": "^2.0.23", "qrcode.vue": "^3.3.3", "qrcodejs2": "^0.0.2", "sockjs-client": "^1.5.2", "sonar-scanner": "^3.1.0", "sortablejs": "^1.15.0", "stompjs": "^2.3.3", "three": "^0.167.0", "vue": "^3.2.6", "vue-i18n": "9.1.10", "vue-router": "^4.0.11", "vue-virtual-scroller": "^2.0.0-beta.8", "vue3-clickout": "^1.1.0", "vuedraggable": "^4.1.0", "webworkify": "^1.5.0" }, "devDependencies": { "@babel/plugin-transform-class-static-block": "^7.26.0", "@commitlint/cli": "^17.1.2", "@commitlint/config-conventional": "^17.1.0", "@cyclonedx/webpack-plugin": "^3.17.0", "@peculiar/x509": "^1.12.3", "@types/crypto-js": "^4.1.1", "@types/file-saver": "^2.0.7", "@types/jest": "^24.0.19", "@types/lodash-es": "^4.17.4", "@types/mapbox__mapbox-gl-geocoder": "^4.7.3", "@types/mapbox__mapbox-sdk": "^0.13.4", "@types/mapbox-gl": "^2.7.11", "@types/node-rsa": "^1.1.1", "@types/sockjs-client": "^1.5.1", "@types/sortablejs": "^1.15.0", "@types/stompjs": "^2.3.5", "@types/three": "^0.165.0", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "@vue-leaflet/vue-leaflet": "^0.6.1", "@vue/babel-plugin-jsx": "^1.1.1", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-typescript": "^5.0.0", "@vue/cli-plugin-unit-jest": "~5.0.0", "@vue/cli-plugin-vuex": "~5.0.0", "@vue/cli-service": "~5.0.0", "@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-typescript": "^9.1.0", "@vue/test-utils": "^2.0.0-0", "asn1js": "^3.0.6", "babel-plugin-import": "^1.13.3", "babel-plugin-lodash": "^3.3.4", "brotli-webpack-plugin": "^1.1.0", "copy-webpack-plugin": "^6.4.0", "cross-env": "^7.0.3", "css-minimizer-webpack-plugin": "^7.0.2", "eslint": "^7.0.0", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-vue": "^8.0.1", "hard-source-webpack-plugin": "^0.13.1", "husky": "^8.0.1", "less": "^3.0.4", "less-loader": "^8.0.0", "lint-staged": "^13.0.3", "mocker-api": "^2.9.0", "mockjs": "^1.1.0", "moment-locales-webpack-plugin": "^1.2.0", "prettier": "^2.2.1", "speed-measure-webpack-plugin": "^1.5.0", "style-resources-loader": "^1.4.1", "svg-sprite-loader": "^6.0.9", "typescript": "4.5.5", "vue-cli-plugin-style-resources-loader": "~0.1.5", "vue-jest": "^5.0.0-0", "webpack-bundle-analyzer": "^4.5.0", "worker-loader": "^3.0.8" }, "resolutions": { "fork-ts-checker-webpack-plugin-v5": "npm:fork-ts-checker-webpack-plugin@^6.0.0" } }
08-30
"dependencies": { "@codemirror/lang-javascript": "^6.1.0", "@codemirror/theme-one-dark": "^6.1.0", "@element-plus/icons-vue": "^2.0.4", "axios": "1.6.5", "codemirror": "^6.0.1", "echarts": "5.3.2", "element-plus": "2.11.5", "js-error-collection": "^1.0.7", "json-editor-vue3": "^1.0.8", "mitt": "3.0.0", "moment-mini": "2.22.1", "nprogress": "0.2.0", "path": "0.12.7", "path-browserify": "^1.0.1", "path-to-regexp": "^6.2.1", "pinia": "^2.3.1", "pinia-plugin-persistedstate": "2.3.0", "screenfull": "^6.0.2", "sortablejs": "^1.15.0", "vue": "^3.5.22", "vue-clipboard3": "^2.0.0", "vue-codemirror": "^6.1.1", "vue-i18n": "9.1.10", "vue-router": "^4.1.5" }, "devDependencies": { "@babel/eslint-parser": "7.16.3", "@originjs/vite-plugin-commonjs": "^1.0.3", "@types/mockjs": "1.0.10", "@types/node": "^17.0.35", "@types/path-browserify": "^1.0.0", "@types/sortablejs": "^1.15.0", "@typescript-eslint/eslint-plugin": "5.30.0", "@typescript-eslint/parser": "5.30.0", "@vitejs/plugin-legacy": "^5.2.0", "@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vitest/coverage-c8": "^0.33.0", "@vitest/ui": "^1.2.0", "@vue/cli-plugin-unit-jest": "4.5.17", "@vue/cli-service": "5.0.8", "@vue/test-utils": "^2.0.2", "@vueuse/core": "^8.7.5", "eslint": "8.18.0", "eslint-config-prettier": "8.5.0", "eslint-define-config": "1.5.1", "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-jsonc": "^2.3.0", "eslint-plugin-markdown": "^3.0.0", "eslint-plugin-prettier": "4.1.0", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unicorn": "^43.0.2", "eslint-plugin-vue": "9.1.1", "husky": "7.0.2", "jsdom": "16.4.0", "jsonc-eslint-parser": "^2.1.0", "majestic": "1.8.1", "mockjs": "1.1.0", "prettier": "2.2.1", "resize-observer-polyfill": "^1.5.1", "rollup-plugin-visualizer": "^5.8.3", "sass": "1.77.6", "svg-sprite-loader": "6.0.11", "typescript": "^4.7.2", "unocss": "^0.58.3", "unplugin-auto-import": "^0.11.2", "unplugin-vue-components": "^0.22.8", "unplugin-vue-define-options": "^0.6.1", "vite": "^7.1.11", "vite-plugin-html": "^3.2.0", "vite-plugin-mkcert": "^1.7.2", "vite-plugin-mock": "^3.0.1", "vite-plugin-style-import": "1.2.1", "vite-plugin-svg-icons": "^2.0.1", "vitest": "^0.22.1", "vue-tsc": "^0.34.16" }, "pnpm": { "peerDependencyRules": { "ignoreMissing": [ "html-webpack-plugin", "vite-plugin-mock", "unplugin-auto-import", "unplugin-vue-components", "vue-template-compiler", "unocss", "unplugin", "vite-plugin-mock", "@vitejs/plugin-legacy", "@vitejs/plugin-vue", "@vitejs/*", "@babel/*", "vite", "vue", "@unocss/vite", "rollup", "vue-jest", "@babel/*" ] } },
11-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值