使用 Vue-router4 和 Vue3 Composition API 进行 Vue 测试实用程序

sho*_*oop 6 typescript vue-test-utils vuejs3 vue-composition-api vue-router4

由于安装过程中设置方法中未定义路由对象,我无法在单元测试期间安装组件。这些指南似乎针对的是 Vue2 和选项 API

\n

参考文献:\n
\n
\n如何编写模拟 vue 组件中 $route 对象的测试\n
\n如何在 vue 组合 api 组件中使用 jest 进行单元测试?\n
\n https://vue-test-utils.vuejs.org/guides/#using-with-typescript \
n \n https://vue-test-utils.vuejs.org/guides/#using-with -vue-路由器

\n

错误

\n
\xe2\x97\x8f CoachItem.vue \xe2\x80\xba displays alert when item is clicked\n\n    TypeError: Cannot read property \'path\' of undefined\n\n      64 |       fullName: computed(() => props.firstName + " " + props.lastName),\n      65 |       coachContactLink: computed(\n    > 66 |         () => route.path + "/" + props.id + "/contact"\n\n
Run Code Online (Sandbox Code Playgroud)\n
// @/tests/unit/example.spec.ts\n\nimport CoachItem from "@/components/coaches/CoachItem.vue"\nimport router from "@/router"\n\n\n  describe("CoachItem.vue", () => {\n    it("displays alert when item is clicked", async () => {\n\n      //const route = { path: \'http://www.example-path.com\' }\n      router.push(\'/\')\n      await router.isReady()\n      const wrapper = mount(CoachItem); //adding this line causes failure\n      //await wrapper.trigger(\'click\');\n      //const dialog = wrapper.find(\'dialog\');\n      //(dialog.exists()).toBeTruthy()\n    })\n  })\n
Run Code Online (Sandbox Code Playgroud)\n
// @/components/UserAlert.vue\n\n<template>\n  <div class="backdrop" @click="closeDialog"></div>\n  <dialog open>\n    <header>\n      <h2>{{ title }}</h2>\n    </header>\n    <div>\n      <slot name="content"></slot>\n    </div>\n    <menu>\n      <button @click="closeDialog">Close</button>\n    </menu>\n  </dialog>\n</template>\n\n<script lang="ts>\nimport { defineComponent } from "vue";\n\nexport default defineComponent({\n  props: [\'title\'],\n  emits: [\'close\'],\n  setup(_, context) {\n    function closeDialog() {\n      context.emit(\'close\');\n    }\n\n    return { closeDialog };\n  },\n});\n</script>\n
Run Code Online (Sandbox Code Playgroud)\n
// @/components/coaches.CoachItem.vue\n\n<template>\n<user-alert v-if="alertIsVisible" title="Alert!" @close="hideAlert">\n    <template v-slot:content><p>this is a slot</p></template>\n  </user-alert>\n  <li @click="showAlert">\n    <h3>{{ fullName }}</h3>\n    <h4>${{ rate }}/hour</h4>\n    <div>\n      <base-badge\n        v-for="area in areas"\n        :key="area"\n        :type="area"\n        :title="area"\n      ></base-badge>\n    </div>\n    <div class="actions">\n      <base-button mode="outline" link :to="coachContactLink"\n        >Contact</base-button\n      >\n      <base-button link :to="coachDetailsLink">View Details</base-button>\n    </div>\n  </li>\n</template>\n\n<script lang="ts">\nimport { computed, defineComponent, PropType, ref } from "vue";\nimport { useRoute } from "vue-router";\nimport useAlert from "../../components/hooks/useAlert";\nexport default defineComponent({\n  props: {\n    id: {\n      type: String,\n      required: true,\n    },\n    firstName: {\n      type: String,\n      required: true,\n    },\n    lastName: {\n      type: String,\n      required: true,\n    },\n    rate: {\n      type: Number,\n      required: true,\n    },\n    areas: {\n      type: Object as PropType<Array<string>>,\n      required: true,\n    },\n  },\n  setup(props) {\n    const route = useRoute();\n    const alertTitle = ref("delete user?");\n    return {\n      fullName: computed(() => props.firstName + " " + props.lastName),\n      coachContactLink: computed(\n        () => route.path + "/" + props.id + "/contact"\n      ),\n      coachDetailsLink: computed(() => route.path + "/" + props.id),\n      ...useAlert()\n    };\n  },\n});\n</script>\n
Run Code Online (Sandbox Code Playgroud)\n
// @/main.ts\nimport { createApp } from "vue";\nimport App from "./App.vue";\nimport router from "./router";\nimport {store, key }  from "./store";\nimport UserAlert from "@/components/UserAlert.vue";\n\ncreateApp(App)\n.component(\'UserAlert\', UserAlert)\n  .use(store, key)\n  .use(router)\n  .mount("#app");\n
Run Code Online (Sandbox Code Playgroud)\n
// @/router/index.ts\n\nconst router = createRouter({\n  history: createWebHistory(process.env.BASE_URL),\n  routes\n});\n\nexport default router;\n
Run Code Online (Sandbox Code Playgroud)\n

Mat*_*att 1

查看vitest Issue #1918中的示例和vue-test-utils 组合文档

以下模拟允许组件使用useRouteruseRoute来工作:

import { mount } from '@vue/test-utils'
import { expect, it, vi } from 'vitest'
import CompWithRoute from './CompWithRoute.vue'

vi.mock('vue-router', () => {
  return {
    useRouter: vi.fn(() => ({
      push: vi.fn(),
    })),
    useRoute: vi.fn(()=> ({
      fullPath: '',
      hash: '',
      matched: [],
      meta: {},
      name: undefined,
      params: {},
      path: '/guppies',
      query: {
        search: 'ireland'
      },
      redirectedFrom: undefined,
    }))
  }
})

it('should render the route loving component', () => {
  const wrapper = mount(CompWithRoute)
})

Run Code Online (Sandbox Code Playgroud)

如果需要测试调用,mockImplementationOnce可以注入间谍(尽管打字稿不喜欢模拟的松散实现Router

import { mount } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import * as routerExports from 'vue-router'
import CompWithRoute from './CompWithRoute.vue'

const useRouter = vi.spyOn(routerExports, 'useRouter')

afterEach(() => {
  vi.clearAllMocks()
})

it('should push a new route on search', async () => {
  const push = vi.fn()
  useRouter.mockImplementationOnce(() => ({
    push
  }))
  const wrapper = mount(CompWithRoute)
  const search = wrapper.find('#search-input')
  await search.setValue('ireland')
  await search.trigger('keyup.enter')
  expect(push).toHaveBeenCalledWith({ query: { search: 'ireland'} })
})
Run Code Online (Sandbox Code Playgroud)

还可以将全局间谍注入到提升的模拟Router实现中,并在您的期望中引用它。

const mock_push = vi.fn()
vi.mock('vue-router', () => ({
  useRouter: vi.fn(() => ({
    push: mock_push,
  })),
}))

afterEach(() => {
  vi.clearAllMocks()
})

it('should push a new route on search', async () => {
  const wrapper = mount(CompWithRoute)
  const search = wrapper.find('#search-input')
  await search.setValue('ireland')
  await search.trigger('keyup.enter')
  expect(mock_push).toHaveBeenCalledWith({ query: { search: 'ireland'} })
})
Run Code Online (Sandbox Code Playgroud)