根據(jù)標(biāo)題,這有可能嗎?
我正在嘗試測(cè)試Vue應(yīng)用程序的一個(gè)簡(jiǎn)單組件,其中包含一個(gè)標(biāo)題和一個(gè)按鈕,該按鈕將用戶重定向到下一個(gè)頁(yè)面。我想測(cè)試一下,當(dāng)按鈕被點(diǎn)擊時(shí),是否會(huì)向路由器發(fā)送一個(gè)事件/消息,并檢查目標(biāo)路由是否正確。
應(yīng)用程序的結(jié)構(gòu)如下:
App.Vue - 入口組件
<script setup lang="ts"> import { RouterView } from "vue-router"; import Wrapper from "@/components/Wrapper.vue"; import Container from "@/components/Container.vue"; import HomeView from "@/views/HomeView.vue"; </script> <template> <Wrapper> <Container> <RouterView/> </Container> </Wrapper> </template> <style> body{ @apply bg-slate-50 text-slate-800 dark:bg-slate-800 dark:text-slate-50; } </style>
router/index.ts - Vue Router配置文件
import {createRouter, createWebHistory} from "vue-router"; import HomeView from "@/views/HomeView.vue"; export enum RouteNames { HOME = "home", GAME = "game", } const routes = [ { path: "/", name: RouteNames.HOME, component: HomeView, alias: "/home" }, { path: "/game", name: RouteNames.GAME, component: () => import("@/views/GameView.vue"), }, ]; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }); export default router;
HomeView.Vue - App組件中RouterView的入口視圖
<template> <Header title="My App"/> <ButtonRouterLink :to="RouteNames.GAME" text="Start" class="btn-game"/> </template> <script setup> import Header from "@/components/Header.vue"; import ButtonRouterLink from "@/components/ButtonRouterLink.vue"; import {RouteNames} from "@/router/"; </script>
ButtonRouterLink.vue
<template> <RouterLink v-bind:to="{name: to}" class="btn"> {{ text }} </RouterLink> </template> <script setup> import { RouterLink } from "vue-router"; const props = defineProps({ to: String, text: String }) </script>
考慮到它使用CompositionAPI和TypeScript,有沒有辦法在Vitest測(cè)試中模擬路由器?
這是一個(gè)測(cè)試文件結(jié)構(gòu)的示例(HomeView.spec.ts):
import {shallowMount} from "@vue/test-utils"; import { describe, test, vi, expect } from "vitest"; import { RouteNames } from "@/router"; import HomeView from "../../views/HomeView.vue"; describe("Home", () => { test ('navigates to game route', async () => { // Check if the button navigates to the game route const wrapper = shallowMount(HomeView); await wrapper.find('.btn-game').trigger('click'); expect(MOCK_ROUTER.push).toHaveBeenCalledWith({ name: RouteNames.GAME }); }); });
我嘗試了多種方法:vi.mock('vue-router'),vi.spyOn(useRoute,'push'),vue-router-mock庫(kù),但是我無法讓測(cè)試運(yùn)行起來,似乎路由器就是不可用的。
根據(jù) @tao 的建議,我意識(shí)到在HomeView中測(cè)試點(diǎn)擊事件并不是一個(gè)很好的測(cè)試(測(cè)試的是庫(kù)而不是我的應(yīng)用程序),所以我添加了一個(gè)測(cè)試 ButtonRouterLink,看看它是否正確渲染了Vue RouterLink,使用to參數(shù):
import {mount, shallowMount} from "@vue/test-utils"; import { describe, test, vi, expect } from "vitest"; import { RouteNames } from "@/router"; import ButtonRouterLink from "../ButtonRouterLink.vue"; vi.mock('vue-router'); describe("ButtonRouterLink", () => { test (`correctly transforms 'to' param into a router-link prop`, async () => { const wrapper = mount(ButtonRouterLink, { props: { to: RouteNames.GAME } }); expect(wrapper.html()).toMatchSnapshot(); }); });
它渲染了一個(gè)空的HTML字符串""
exports[`ButtonRouterLink > correctly transforms 'to' param into a router-link prop 1`] = `""`;
伴隨著一個(gè)不太有用的Vue警告(未指定預(yù)期類型):
[Vue warn]: Invalid prop: type check failed for prop "to". Expected , got Object at <RouterLink to= { name: 'game' } class="btn btn-blue" > at <ButtonRouterLink to="game" ref="VTU_COMPONENT" > at <VTUROOT> [Vue warn]: Invalid prop: type check failed for prop "ariaCurrentValue". Expected , got String with value "page". at <RouterLink to= { name: 'game' } class="btn btn-blue" > at <ButtonRouterLink to="game" ref="VTU_COMPONENT" > at <VTUROOT> [Vue warn]: Component is missing template or render function. at <RouterLink to= { name: 'game' } class="btn btn-blue" > at <ButtonRouterLink to="game" ref="VTU_COMPONENT" > at <VTUROOT>
這是可能的。
你的示例和官方示例之間有一個(gè)重要的區(qū)別:在被測(cè)試的組件中,你將其放置在了一個(gè)中間組件中,而不是使用RouterLink
(或使用router.push
的按鈕)。
這會(huì)如何改變單元測(cè)試?
第一個(gè)問題是你使用了shallowMount
,它會(huì)用空容器替換子組件。在你的情況下,這意味著ButtonRouterLink
不再包含RouterLink
,并且不會(huì)對(duì)點(diǎn)擊事件做任何操作,它只是接收點(diǎn)擊事件。
解決這個(gè)問題的選項(xiàng)有:
mount
而不是shallowMount
,將其變成集成測(cè)試而不是單元測(cè)試ButtonRouterLink
的測(cè)試中。一旦測(cè)試了ButtonRouterLink
的任何:to
是否正確轉(zhuǎn)換為{ name: to }
并傳遞給RouterLink
,你只需要測(cè)試這些:to
是否正確傳遞給使用它們的任何組件中的<ButtonRouterLink />
。將這個(gè)功能移動(dòng)到自己的組件中所創(chuàng)建的第二個(gè)問題稍微微妙一些。通常,我期望以下ButtonRouterLink的測(cè)試可以工作:
import { RouterLinkStub } from '@vue/test-utils' const wrapper = shallowMount(YourComp, { stubs: { RouterLink: RouterLinkStub } }) expect(wrapper.findComponent(RouterLinkStub).props('to')).toEqual({ name: RouterNames.GAME })
令我驚訝的是,這個(gè)測(cè)試失敗了,它聲稱在RouterLinkStub中接收到的:to
屬性值不是{ name: RouterNames.GAME }
,而是'game'
,這是我傳遞給ButtonRouterLink
的值。就像我們的組件從未將value
轉(zhuǎn)換為{ name: value }
一樣。
相當(dāng)奇怪。
事實(shí)證明,問題是<RouterLink />
恰好是我們組件的根元素。這意味著組件(<BottomRouterLink>
)在DOM中被<RouterLinkStub>
替換,但:to
的值是從前者而不是后者讀取的。簡(jiǎn)而言之,如果模板是這樣的,測(cè)試就會(huì)成功:
<span> <RouterLink ... /> </span>
為了解決這個(gè)問題,我們需要導(dǎo)入實(shí)際的RouterLink
組件(來自vue-router
),并找到它,而不是找到RouterLinkStub
。為什么?@vue/test-utils
足夠聰明,可以找到存根組件而不是實(shí)際組件,但是這樣,.findComponent()
只會(huì)匹配替換后的元素,而不是在它仍然是ButtonRouterLink
(未處理)時(shí)。此時(shí),:to
的值已經(jīng)被解析為RouterLink
實(shí)際接收的值。
完整的測(cè)試如下:
import { shallowMount } from '@vue/test-utils' import { describe, it, expect } from 'vitest' import ButtonRouterLink from '../src/ButtonRouterLink.vue' import { RouterLinkStub } from '@vue/test-utils' import { RouterLink } from 'vue-router' const TEST_STRING = 'this is a test string' describe('ButtonRouterLink', () => { it(`should transform 'to' into '{ name: to }'`, () => { const wrapper = shallowMount(ButtonRouterLink, { props: { to: TEST_STRING }, stubs: { RouterLink: RouterLinkStub } }) expect(wrapper.findComponent(RouterLink).props('to')).toEqual({ name: TEST_STRING }) }) })
有點(diǎn)棘手。