Судя по заголовку, это вообще возможно?
Я пытаюсь протестировать простой компонент приложения Vue, который содержит заголовок и кнопку, которая направляет пользователя на следующую страницу. Я хочу проверить, что после нажатия кнопок событие/сообщение отправляется на маршрутизатор, и проверьте правильность маршрута назначения.
Структура приложения:
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 — представление входа для 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?
Вот пример структуры тестового файла (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, и мне всегда не удавалось запустить тесты вроде роутера просто нет.
Следуя совету @tao, я понял, что тестирование щелчка в HomeView не очень хороший тест (тестирование библиотек, а не моего приложения), поэтому я добавил тест для ButtonRouterLink, чтобы увидеть, правильно ли он отображает Vue RouterLink, используя to параметр:
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();
});
});
Что отображает пустую строку HTML ""
exports[`ButtonRouterLink > correctly transforms 'to' param into a router-link prop 1`] = `""`;
сопровождается довольно бесполезным предупреждением Vue (ожидаемые типы не указаны):
[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>
Это возможно.
Между вашим примером и официальным есть одно важное отличие: вместо использования RouterLink
(или кнопки, которая использует router.push
) внутри тестируемого компонента вы поместили его в промежуточный компонент.
Как это меняет модульный тест?
Первая проблема заключается в том, что вы используете smallMount, который заменяет дочерние компоненты пустыми контейнерами. В вашем случае это означает, что ButtonRouterLink
больше не содержит RouterLink
и ничего не делает с кликом. Только получает.
Варианты обойти это:
mount
вместо shallowMount
, чтобы сделать это интеграционным тестом, а не единичнымButtonRouterLink
. После того, как вы проверите, что любой :to
в ButtonRouterLink правильно преобразован в { name: to }
и передан в RouterLink, все, что вам нужно проверить, это правильность передачи :to
в <ButtonRouterLink />
в любом компоненте, использующем их.Вторая проблема, которую создает перенос этой функциональности в отдельный компонент, немного более тонкая. Обычно я ожидал, что следующий тест ButtonRouterLink сработает:
import { RouterLinkStub } from '@vue/test-utils'
const wrapper = shallowMount(YourComp, {
stubs: {
RouterLink: RouterLinkStub
}
})
expect(wrapper.findComponent(RouterLinkStub).props('to')).toEqual({
name: RouterNames.GAME
})
К моему удивлению, тест провалился, утверждая, что полученное значение свойства :to
в RouterLinkStub было не { name: RouterNames.GAME }
, а 'game'
, что я и передал ButtonRouterLink
. Как будто наш компонент никогда не выполнял преобразование из value
в { name: value }
.
Довольно странно.
Оказывается, проблема в том, что <RouterLink />
является корневым элементом нашего компонента. Это означает, что компонент (<BottomRouterLink>
) заменялся в DOM на <RouterLinkStub>
, но значение :to
читалось из первого, а не из второго. Короче говоря, тест сработал бы, если бы шаблон был:
<span>
<RouterLink ... />
</span>
Чтобы обойти это, нам нужно импортировать фактический компонент RouterLink
(из vue-router
) и найти его вместо поиска RouterLinkStub
. Почему? @vue/test-utils
достаточно умен, чтобы найти компонент-заглушку вместо фактического, но таким образом .findComponent()
будет соответствовать элементу только после замены, а не тогда, когда он все еще был ButtonRouterLink
(необработанным). В этот момент значение :to
было преобразовано в то, что на самом деле получает RouterLink
.
Полный тест выглядит так:
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
})
})
})
Сложный.
@h0nter, мне пришлось раскручивать проект пару часов, пока я не докопался до сути, просмотрев документы и заставив консоль истекать кровью. Смотрите обновление к ответу.
@Pipetus, ты считаешь, что я был бы лучше, если бы не давал тебе совет? Потому что я верю в обратное. Основное различие между нашими определениями приятности заключается в том, что вы рассматриваете чувства человека в очень коротком временном масштабе, тогда как я имею в виду более длительный период времени. Забавно то, что вас запоминают по тому, что люди чувствовали в данный момент, а не по тому, как вы заставляете их чувствовать себя в будущем, если вам удастся изменить их к лучшему. Но я думаю, это моя проблема. Спасибо за то, что был добр и честен со мной. Удачного кодирования!
Это действительно впечатляющая работа по отладке, большое спасибо за вашу помощь в этом. Теперь все понятно, почему он не будет правильно соответствовать объекту, поэтому он отвечает на мой вопрос. В вашем ответе есть две вещи, которые я хотел проверить: во-первых, упаковка ButtonRouterLink в <span>, похоже, не имеет никакого эффекта, поскольку ошибки теста: Error: Cannot call props on an empty VueWrapper.
поскольку wrapper.findComponent(RouterLinkStub)
является нулевым объектом. Во-вторых, в вашем окончательном ответе, я думаю, нам не нужно указывать заглушку RouterLink, так как smallMount обработает ее автоматически.
Я удивлен, что это не работает, когда это не корневой элемент, это не имеет смысла. RouterLinkStub
никогда не должен иметь :to
равное строковому значению, он всегда должен быть объектом, за исключением случаев, когда он заменяет элемент строкой :to
. Возможно, я посмотрю на это позже, но точно не сегодня.
Спасибо и не волнуйтесь, это больше не проблема, так как вы ответили на мой вопрос, и у меня есть рабочий тест. Теперь мне просто любопытно, почему это не работает последовательно.
Спасибо за такой исчерпывающий ответ, особенно совет со снимками против console.info(), полезно понять, что на самом деле отображается. Кроме того, я вижу, что нет особого смысла тестировать триггер кнопки HomeView, так как я не тестирую там никаких пользовательских поведений. Я попытался протестировать ButtonRouterLink, как вы предложили, я использовал mount для компонента, передал имя тестового маршрута в качестве параметра и сделал снимок, чтобы увидеть, что отображается, но он отобразил пустую строку «», сопровождаемую довольно бесполезным предупреждением Vue (обновленный вопрос с тестом и предупреждающим сообщением).