# 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. - Published: 2026-06-21 - Updated: 2026-07-06 - Author: SharpSkill - Tags: vue, testing, vitest, vue-test-utils, interview - Reading time: 10 min --- 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](https://vitest.dev/guide/) pour l'exécution et [Vue Test Utils](https://test-utils.vuejs.org/) 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](https://vuejs.org/guide/scaling-up/testing.html) 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. ```typescript // vitest.config.ts 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. ```typescript // PriceTag.spec.ts 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. ```typescript // SearchBar.spec.ts 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](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns). ```typescript // useCart.spec.ts 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. ## 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. ```typescript // useProducts.spec.ts 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](https://pinia.vuejs.org/cookbook/testing.html) recommande cette approche pour les tests au niveau du composant. ```typescript // CheckoutButton.spec.ts 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. ```typescript // vitest.config.ts (coverage section) 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](/technologies/vue-nuxt/interview-questions/vue-testing). **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](/blog/vue-nuxt/essential-vuejs-interview-questions). ## 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](/technologies/vue-nuxt) au complet pour continuer à bâtir des compétences prêtes pour l'entretien. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/vue-nuxt/vue-3-testing-vitest-vue-test-utils