Maison  >  Questions et réponses  >  le corps du texte

Mock Vue RouterView/RouterLink dans Vitest (API combinée)

D'après le titre, est-ce possible ?

J'essaie de tester un composant simple d'une application Vue qui contient un en-tête et un bouton qui redirige l'utilisateur vers la page suivante. Je veux tester si lorsque vous cliquez sur le bouton, un événement/message est envoyé au routeur et vérifier si l'itinéraire de destination est correct.

La candidature est structurée comme suit :

App.Vue - Composant d'entrée

<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 - Fichier de configuration de 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 - La vue d'entrée de RouterView dans le composant App

<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>

Étant donné qu'il utilise CompositionAPI et TypeScript, existe-t-il un moyen de simuler le routeur dans les tests Vitest ?

Voici un exemple de structure de fichier de test (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 });
    });
});

J'ai essayé plusieurs méthodes : vi.mock('vue-router'), vi.spyOn(useRoute,'push'), bibliothèque vue-router-mock, mais je n'arrive pas à exécuter le test, il semble que le routeur n'est tout simplement pas disponible.

Mise à jour (15/04/23)

Suite à la suggestion de @tao, j'ai réalisé que tester les événements de clic dans HomeView n'était pas un bon test (c'était la bibliothèque qui a été testée, pas mon application), j'ai donc ajouté un test ButtonRouterLink pour voir si Vue RouterLink est rendu correctement , en utilisant les paramètres 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();
    });
});

Il affiche une chaîne HTML vide""

exports[`ButtonRouterLink > correctly transforms 'to' param into a router-link prop 1`] = `""`;

Accompagné d'un avertissement Vue pas si utile (type attendu non spécifié) :

[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>

P粉478188786P粉478188786205 Il y a quelques jours393

répondre à tous(1)je répondrai

  • P粉759451255

    P粉7594512552024-03-28 00:46:02

    C'est possible.

    Il y a une différence importante entre votre exemple et l'exemple officiel : dans le composant testé, vous l'avez placé dans un composant intermédiaire au lieu d'utiliser un bouton RouterLink(或使用router.push).

    Comment cela va-t-il changer les tests unitaires ?

    Le premier problème est que vous utilisez shallowMountshallowMount,它会用空容器替换子组件。在你的情况下,这意味着ButtonRouterLink不再包含RouterLink, qui remplace le composant enfant par un conteneur vide. Dans votre cas, cela signifie que ButtonRouterLink ne contient plus RouterLink et ne fait rien avec les événements de clic, il reçoit simplement les événements de clic.

    Les options pour résoudre ce problème sont :

    • a) Utilisez mount而不是shallowMount et transformez-le en test d'intégration au lieu d'un test unitaire
    • b) Déplacez les tests pour cette fonctionnalité vers ButtonRouterLink的测试中。一旦测试了ButtonRouterLink的任何:to是否正确转换为{ name: to }并传递给RouterLink,你只需要测试这些:to是否正确传递给使用它们的任何组件中的<ButtonRouterLink />.

    Le deuxième problème créé par le déplacement de cette fonctionnalité dans son propre composant est légèrement plus subtil. En général, je m'attendrais à ce que le test suivant pour ButtonRouterLink fonctionne :

    import { RouterLinkStub } from '@vue/test-utils'
    
    const wrapper = shallowMount(YourComp, {
      stubs: {
        RouterLink: RouterLinkStub
      }
    })
    
    expect(wrapper.findComponent(RouterLinkStub).props('to')).toEqual({
      name: RouterNames.GAME
    })
    

    À ma grande surprise, ce test échoue avec le même :to属性值不是{ name: RouterNames.GAME },而是'game',这是我传递给ButtonRouterLink的值。就像我们的组件从未将value转换为{ name: value }il prétend être reçu dans le RouterLinkStub.

    Assez étrange.
    Il s’avère que le problème est que la valeur de <RouterLink />恰好是我们组件的根元素。这意味着组件(<BottomRouterLink>)在DOM中被<RouterLinkStub>替换,但:to est lue à partir de la première et non de la seconde. Bref, si le modèle ressemble à ceci, le test réussira :

      <span>
        <RouterLink ... />
      </span>
    

    Pour résoudre ce problème, nous devons importer la valeur réelle RouterLink组件(来自vue-router),并找到,而不是找到RouterLinkStub。为什么?@vue/test-utils足够聪明,可以找到存根组件而不是实际组件,但是这样,.findComponent()只会匹配替换后的元素,而不是在它仍然是ButtonRouterLink(未处理)时。此时,:to的值已经被解析为RouterLinkreçue.

    Le test complet est le suivant :

    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
        })
      })
    })

    Un peu délicat.

    répondre
    0
  • Annulerrépondre