Tester Vue 3 en 2026 : Vitest, Vue Test Utils et questions d'entretien

Un guide pratique pour tester une application Vue en 2026 : configurer Vitest, monter des composants avec Vue Test Utils, tester composables et stores Pinia, simuler des API, mesurer la couverture et répondre aux questions d'entretien.

Flux de test Vue avec Vitest et Vue Test Utils en 2026

Tester une application Vue distingue le refactoring serein du bricolage hasardeux, et en 2026 la chaîne d'outils s'est resserrée autour de deux bibliothèques : Vitest pour l'exécution et Vue Test Utils pour le montage des composants. Ce guide détaille la configuration de la stack, les tests de composants et de composables, la simulation des dépendances, ainsi que les questions d'entretien sur les tests Vue que les équipes de recrutement posent réellement.

La stack de test Vue moderne en 2026

La recommandation par défaut de la documentation Vue tient en trois choix : Vitest pour les tests unitaires et de composants, Vue Test Utils comme API de montage bas niveau, et Playwright ou Cypress pour la couverture de bout en bout. Vitest réutilise la configuration Vite de l'application, si bien que les tests passent par la même chaîne de transformation avec un rechargement quasi instantané.

Configurer Vitest pour un projet Vue 3

Vitest partage le pipeline de Vite, ce qui signifie qu'un seul fichier de configuration pilote à la fois le serveur de développement et l'exécuteur de tests. L'option environment doit valoir jsdom ou happy-dom pour que les tests de composants disposent d'un DOM où effectuer le rendu. Le drapeau globals: true expose describe, it et expect sans avoir à les importer dans chaque fichier.

vitest.config.tstypescript
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  test: {
    // jsdom provides document/window so components can mount
    environment: 'jsdom',
    // expose describe/it/expect globally
    globals: true,
    // run this before each test file (matchers, cleanup)
    setupFiles: ['./tests/setup.ts'],
    // only pick up *.spec.ts and *.test.ts files
    include: ['**/*.{test,spec}.ts'],
  },
  resolve: {
    alias: {
      // mirror the @ alias used across the app
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

Avec Vitest 3, la configuration ci-dessus fonctionne d'emblée après l'installation de vitest, @vue/test-utils, @vitejs/plugin-vue et jsdom. Comme l'alias reproduit la configuration de l'application, des imports tels que @/composables/useCart se résolvent de façon identique en test et en production.

Monter des composants avec Vue Test Utils

Vue Test Utils propose deux points d'entrée : mount, qui effectue le rendu de l'arbre de composants complet, et shallowMount, qui remplace les composants enfants par des stubs. Pour la plupart des tests unitaires, mount reste préférable car il éprouve le comportement de rendu réel. Le wrapper renvoyé fournit des aides de requête comme find, get et text pour formuler des assertions sur la sortie rendue.

PriceTag.spec.tstypescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import PriceTag from '@/components/PriceTag.vue'

describe('PriceTag', () => {
  it('formats the amount as currency', () => {
    const wrapper = mount(PriceTag, {
      // props are passed through the mounting options
      props: { amount: 4200, currency: 'EUR' },
    })

    // get() throws if the selector is missing, unlike find()
    expect(wrapper.get('[data-test="price"]').text()).toBe('€42.00')
  })

  it('applies a discount class when on sale', () => {
    const wrapper = mount(PriceTag, {
      props: { amount: 4200, currency: 'EUR', onSale: true },
    })

    // classes() returns the array of applied CSS classes
    expect(wrapper.get('[data-test="price"]').classes()).toContain('price--sale')
  })
})

Cibler les éléments via des attributs data-test plutôt que par des classes CSS rend les tests résistants : une modification de style sur .price--sale ne cassera pas le sélecteur, seul un vrai changement de comportement le fera. Ce découplage est un fil rouge des tests Vue faciles à maintenir.

Tester les événements utilisateur et les événements émis

Les composants interactifs exigent des assertions sur ce qui se produit après un clic ou une saisie. Vue Test Utils renvoie une promesse depuis trigger, et l'attendre vide la file de réactivité de Vue afin que le DOM reflète la mise à jour. L'aide emitted enregistre chaque événement personnalisé émis par un composant, ce qui permet de vérifier les contrats entre parent et enfant.

SearchBar.spec.tstypescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import SearchBar from '@/components/SearchBar.vue'

describe('SearchBar', () => {
  it('emits search with the typed query', async () => {
    const wrapper = mount(SearchBar)

    // setValue writes to the input and fires an input event
    await wrapper.get('input').setValue('vitest')
    // awaiting trigger flushes the reactivity queue before assertions
    await wrapper.get('[data-test="submit"]').trigger('click')

    // emitted() returns a record of every event and its payloads
    const events = wrapper.emitted('search')
    expect(events).toHaveLength(1)
    // events[0] is the first emission, [0] its first argument
    expect(events![0][0]).toBe('vitest')
  })
})
Toujours attendre les mises à jour du DOM

Vue regroupe les mises à jour réactives de manière asynchrone. Oublier d'attendre un appel à trigger ou setValue est la cause la plus fréquente de tests Vue instables : les assertions s'exécutent avant le nouveau rendu du DOM et lisent donc une sortie périmée. Lorsqu'un changement d'état survient hors d'une aide d'événement, il faut importer nextTick depuis vue puis attendre nextTick() avant l'assertion.

Tester les composables Vue de manière isolée

Les composables qui n'utilisent que des API de réactivité (ref, computed, watch) ne réclament aucun composant : ce sont de simples fonctions renvoyant un état réactif, si bien que les appeler directement et vérifier .value suffit. C'est la manière la plus rapide et la plus ciblée de couvrir la logique métier, et elle se marie bien avec les approches décrites dans les composables Vue 3 avancés.

useCart.spec.tstypescript
import { describe, it, expect } from 'vitest'
import { useCart } from '@/composables/useCart'

describe('useCart', () => {
  it('tracks total price as items change', () => {
    // composables using only ref/computed run without mounting
    const { items, total, addItem } = useCart()

    expect(total.value).toBe(0)

    addItem({ id: 1, price: 1500, qty: 2 })
    // computed total recalculates synchronously on state change
    expect(total.value).toBe(3000)

    addItem({ id: 2, price: 500, qty: 1 })
    expect(items.value).toHaveLength(2)
    expect(total.value).toBe(3500)
  })
})

Les composables qui dépendent de hooks de cycle de vie tels que onMounted font exception : ils doivent s'exécuter à l'intérieur d'une instance de composant. La solution courante consiste en un minuscule composant d'aide qui appelle le composable dans setup et en expose le résultat, puis à monter ce composant d'aide avec Vue Test Utils.

Prêt à réussir tes entretiens Vue.js / Nuxt.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Simuler les appels d'API avec vi.mock

Les vraies requêtes réseau rendent les tests lents et non déterministes. Vitest remplace des modules avec vi.mock et crée des fonctions espionnes avec vi.fn. Le motif ci-dessous simule le module qui encapsule fetch, de sorte que le composable testé reçoit des données maîtrisées sans jamais atteindre un serveur.

useProducts.spec.tstypescript
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useProducts } from '@/composables/useProducts'
import { getProducts } from '@/api/products'

// replace the whole module with mocked exports
vi.mock('@/api/products', () => ({
  getProducts: vi.fn(),
}))

describe('useProducts', () => {
  beforeEach(() => {
    // reset call history between tests to avoid leakage
    vi.mocked(getProducts).mockReset()
  })

  it('exposes fetched products and clears loading', async () => {
    // define what the mocked API returns for this test
    vi.mocked(getProducts).mockResolvedValue([
      { id: 1, name: 'Keyboard' },
    ])

    const { products, loading, load } = useProducts()
    await load()

    expect(getProducts).toHaveBeenCalledOnce()
    expect(loading.value).toBe(false)
    expect(products.value[0].name).toBe('Keyboard')
  })
})

Simuler à la frontière du module laisse la logique interne du composable intacte tout en donnant à chaque test la maîtrise totale de la réponse d'API, chemins d'erreur compris. Appeler mockRejectedValue à la place permet à un test isolé de vérifier qu'un échec positionne un état d'erreur et arrête l'indicateur de chargement.

Tester les stores Pinia en 2026

Les stores conservent l'état partagé entre composants, et le paquet officiel @pinia/testing en simplifie le test. createTestingPinia remplace chaque action par un stub par défaut, si bien qu'un test de composant peut vérifier qu'une action a été déclenchée sans exécuter ses effets de bord. Le guide de test de Pinia recommande cette approche pour les tests au niveau du composant.

CheckoutButton.spec.tstypescript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CheckoutButton from '@/components/CheckoutButton.vue'
import { useCartStore } from '@/stores/cart'

describe('CheckoutButton', () => {
  it('dispatches checkout on click', async () => {
    const wrapper = mount(CheckoutButton, {
      global: {
        plugins: [
          // vi.fn spies on actions instead of executing them
          createTestingPinia({ createSpy: vi.fn }),
        ],
      },
    })

    const store = useCartStore()
    await wrapper.get('[data-test="checkout"]').trigger('click')

    // the action is stubbed, so only the dispatch is asserted
    expect(store.checkout).toHaveBeenCalledOnce()
  })
})

Pour tester la logique d'un store elle-même plutôt que son intégration dans un composant, il faut passer stubActions: false afin que les actions s'exécutent normalement, puis formuler des assertions sur l'état obtenu. Cette séparation entre tests de composants à actions neutralisées et tests de store à actions réelles maintient chaque suite concentrée sur une responsabilité unique.

Exécuter les tests en mode watch et mesurer la couverture

Vitest embarque un mode watch qui ne relance que les tests affectés par un fichier enregistré, ce qui ramène la boucle de retour, pendant le développement, à une fraction de seconde. Le rapport de couverture est intégré via le fournisseur @vitest/coverage-v8 et s'insère proprement dans un pipeline d'intégration continue, où un test en échec ou une régression de couverture peut bloquer une fusion.

vitest.config.ts (coverage section)typescript
export default defineConfig({
  test: {
    coverage: {
      // v8 is the fastest provider and needs no instrumentation step
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      // fail CI when a threshold drops below the target
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
      },
      // exclude generated and config files from the report
      exclude: ['**/*.config.ts', '**/types/**'],
    },
  },
})

Lancer vitest seul démarre le mode watch en local, tandis que vitest run --coverage exécute une passe unique adaptée à la CI. Fixer des seuils explicites transforme la couverture, de métrique de façade, en garde-fou de qualité imposé : une pull request qui fait chuter la couverture de branches sous 75 pour cent échoue automatiquement, ce qui pousse les contributeurs à tester les chemins qu'ils ajoutent plutôt que le seul cas nominal.

Questions d'entretien fréquentes sur les tests Vue et réponses

Les questions sur les tests reviennent dans la plupart des entretiens Vue seniors car elles révèlent la façon dont un candidat pense la maintenabilité. Les réponses ci-dessous couvrent les concepts les plus récurrents, et l'ensemble complet est travaillé dans le module d'entretien sur les tests Vue.

Quand faut-il utiliser shallowMount plutôt que mount ? On se tourne vers shallowMount lorsqu'un composant rend des enfants lourds ou déjà testés et que le test ne s'intéresse qu'à la logique propre du parent. Neutraliser les enfants isole l'unité et accélère le rendu, au prix de ne pas vérifier l'intégration réelle entre composants.

Comment gérer les mises à jour asynchrones dans Vue Test Utils ? Vue applique les changements réactifs au tick suivant. Les aides d'événement comme trigger et setValue renvoient des promesses qui se résolvent une fois le DOM mis à jour, il suffit donc de les attendre. Pour un état modifié en dehors de ces aides, attendre nextTick() force la file à se vider avant l'exécution des assertions.

Quelle est la différence entre find et get ? find renvoie un wrapper vide quand aucun élément ne correspond et ne lève jamais d'erreur, ce qui convient à des assertions comme expect(wrapper.find('.error').exists()).toBe(false). get lève immédiatement une erreur en l'absence d'élément, ce qui en fait le meilleur choix lorsque l'élément est censé exister et que son absence constitue un véritable échec.

Pourquoi préférer les attributs data-test aux sélecteurs de classe ? Les noms de classe évoluent avec le style et la structure, si bien que les tests qui les interrogent se cassent au moindre ajustement cosmétique. Un point d'ancrage data-test dédié exprime explicitement l'intention de test et survit aux refactorings, réduisant les faux négatifs.

Comment tester un composable qui repose sur onMounted ? Les hooks de cycle de vie ne se déclenchent qu'à l'intérieur d'une instance de composant, le composable ne peut donc pas être appelé directement. La technique standard passe par un composant d'échafaudage jetable qui invoque le composable dans setup et en expose la valeur de retour, puis monte cet échafaudage avec Vue Test Utils pour formuler les assertions via le wrapper. On préserve ainsi le câblage de la réactivité et du cycle de vie tout en isolant la logique testée. D'autres questions conceptuelles et de code sont rassemblées dans le guide des questions d'entretien Vue.js essentielles.

Conclusion

Un dispositif de test Vue fiable en 2026 tient à quelques choix délibérés :

  • Exécuter Vitest avec environment: 'jsdom' et le plugin Vue afin que les tests partagent la chaîne de transformation Vite exacte de l'application.
  • Préférer mount à shallowMount sauf si un composant enfant est lourd ou déjà couvert par ailleurs.
  • Toujours attendre trigger, setValue et nextTick avant une assertion pour éliminer les tests instables dépendants du timing.
  • Tester les composables qui n'utilisent que des API de réactivité en les appelant directement et en vérifiant .value.
  • Simuler à la frontière du module avec vi.mock pour rendre le comportement réseau déterministe et les chemins d'erreur testables.
  • Employer createTestingPinia({ createSpy: vi.fn }) pour les tests de composants et stubActions: false pour éprouver la logique d'un store.
  • Interroger les éléments via des attributs data-test afin que les tests ne cassent que sur un vrai changement de comportement, jamais sur le style.

Maîtriser ces motifs fait du test un outil de conception plutôt qu'une corvée, capable d'attraper les régressions avant qu'elles n'atteignent la production. Parcourez le parcours d'apprentissage Vue et Nuxt au complet pour continuer à bâtir des compétences prêtes pour l'entretien.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#vue
#testing
#vitest
#vue-test-utils
#interview

Partager

Articles similaires