Testing en Vue 3 en 2026: Vitest, Vue Test Utils y preguntas de entrevista

Guía práctica de testing en Vue para 2026: configurar Vitest, montar componentes con Vue Test Utils, probar composables y stores de Pinia, simular APIs, medir cobertura y responder las preguntas de entrevista que hacen los equipos de contratación.

Flujo de testing en Vue con Vitest y Vue Test Utils en 2026

El testing en Vue marca la diferencia entre refactorizar con confianza y avanzar a ciegas. En 2026 el conjunto de herramientas se consolidó en torno a dos librerías: Vitest como corredor de pruebas y Vue Test Utils para montar componentes. Esta guía recorre la configuración del stack, cómo probar componentes y composables, cómo simular dependencias y cómo responder las preguntas de entrevista sobre testing en Vue que realmente formulan los equipos de contratación.

El stack moderno de testing en Vue en 2026

La recomendación por defecto de la documentación de Vue es Vitest para pruebas unitarias y de componentes, Vue Test Utils como API de montaje de bajo nivel, y Playwright o Cypress para la cobertura de extremo a extremo. Vitest reutiliza la misma configuración de Vite que la aplicación, así que las pruebas atraviesan idéntica cadena de transformación con recarga en caliente casi instantánea.

Configurar Vitest en un proyecto Vue 3

Vitest comparte la cadena de Vite, lo que significa que un único archivo de configuración gobierna tanto el servidor de desarrollo como el corredor de pruebas. La opción environment debe fijarse en jsdom o happy-dom para que las pruebas de componentes dispongan de un DOM donde renderizar. El flag globals: true expone describe, it y expect sin necesidad de importarlos en cada archivo.

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

Con Vitest 3 la configuración anterior funciona de inmediato tras instalar vitest, @vue/test-utils, @vitejs/plugin-vue y jsdom. Como el alias replica la configuración de la aplicación, importaciones como @/composables/useCart se resuelven de forma idéntica en las pruebas y en producción.

Montar componentes con Vue Test Utils

Vue Test Utils ofrece dos puntos de entrada: mount, que renderiza el árbol de componentes completo, y shallowMount, que reemplaza los componentes hijos por stubs. Para la mayoría de las pruebas unitarias se prefiere mount, porque ejercita el comportamiento real de renderizado. El wrapper devuelto aporta ayudantes de consulta como find, get y text para verificar la salida renderizada.

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

Apuntar a los elementos mediante atributos data-test en lugar de clases CSS mantiene las pruebas resistentes: un cambio de estilo en .price--sale no romperá el selector, solo lo hará un cambio genuino de comportamiento. Este desacoplamiento es un tema recurrente en el testing mantenible de Vue.

Probar eventos de usuario y eventos emitidos

Los componentes interactivos necesitan verificar qué ocurre tras un clic o una entrada de texto. Vue Test Utils devuelve una promesa desde trigger, y esperarla con await vacía la cola de reactividad de Vue para que el DOM refleje la actualización. El ayudante emitted registra cada evento personalizado que disparó el componente, y así se verifican los contratos entre padre e hijo.

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')
  })
})
Siempre espera las actualizaciones del DOM

Vue agrupa las actualizaciones reactivas de forma asíncrona. Olvidar el await en una llamada a trigger o setValue es la causa más habitual de pruebas inestables en Vue: las verificaciones se ejecutan antes de que el DOM vuelva a renderizar, por lo que leen una salida obsoleta. Cuando un cambio de estado ocurre fuera de un ayudante de eventos, hay que importar nextTick desde vue y hacer await nextTick() antes de verificar.

Probar composables de Vue de forma aislada

Los composables que solo emplean APIs de reactividad (ref, computed, watch) no requieren ningún componente: son funciones planas que devuelven estado reactivo, así que basta con invocarlas directamente y verificar sobre .value. Esta es la manera más rápida y enfocada de cubrir la lógica de negocio, y combina bien con los patrones descritos en composables avanzados de Vue 3.

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

Los composables que dependen de hooks del ciclo de vida como onMounted son la excepción: deben ejecutarse dentro de una instancia de componente. El truco habitual es un componente auxiliar diminuto que llama al composable en setup y expone el resultado, y luego se monta ese auxiliar con Vue Test Utils.

¿Listo para aprobar tus entrevistas de Vue.js / Nuxt.js?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Simular llamadas a la API con vi.mock

Las peticiones de red reales vuelven las pruebas lentas y no deterministas. Vitest reemplaza módulos con vi.mock y crea funciones espía con vi.fn. El patrón siguiente simula el módulo que envuelve a fetch, de modo que el composable bajo prueba recibe datos controlados sin llegar jamás a un servidor.

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

Simular en la frontera del módulo deja intacta la lógica interna del composable a la vez que otorga a cada prueba control total sobre la respuesta de la API, incluidos los caminos de error. Usar mockRejectedValue en su lugar permite que una sola prueba verifique que las fallas establecen un estado de error y detienen el spinner.

Probar stores de Pinia en 2026

Los stores concentran el estado compartido entre componentes, y el paquete oficial @pinia/testing los hace fáciles de probar. createTestingPinia convierte cada acción en un stub por defecto, de modo que una prueba de componente puede verificar que una acción fue despachada sin ejecutar sus efectos secundarios. La guía de testing de Pinia recomienda este enfoque para las pruebas a nivel de componente.

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

Para probar la lógica del store en sí y no la integración con el componente, se pasa stubActions: false para que las acciones se ejecuten con normalidad, y luego se verifica el estado resultante. Esta separación entre pruebas de componente con acciones simuladas y pruebas de store con acciones reales mantiene cada suite enfocada en una única responsabilidad.

Ejecutar pruebas en modo watch y medir la cobertura

Vitest incluye un modo watch que vuelve a ejecutar únicamente las pruebas afectadas por un archivo guardado, lo que acorta el ciclo de retroalimentación durante el desarrollo a una fracción de segundo. El reporte de cobertura viene incorporado a través del proveedor @vitest/coverage-v8 y se integra sin fricción en una cadena de integración continua, donde una prueba fallida o una regresión de cobertura puede bloquear una fusión.

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/**'],
    },
  },
})

Ejecutar vitest sin más inicia el modo watch en local, mientras que vitest run --coverage realiza una única pasada apropiada para CI. Definir umbrales explícitos convierte la cobertura de una métrica cosmética en una barrera de calidad exigida: un pull request que baja la cobertura de ramas por debajo del 75 por ciento falla de forma automática, lo que empuja a quienes contribuyen a probar los caminos que agregan y no solo el escenario feliz.

Preguntas frecuentes de entrevista sobre testing en Vue con respuestas

Las preguntas sobre testing aparecen en la mayoría de las entrevistas senior de Vue porque revelan cómo razona una persona candidata acerca de la mantenibilidad. Las respuestas siguientes cubren los conceptos que surgen con más frecuencia, y el conjunto completo se practica en el módulo de entrevista sobre testing en Vue.

¿Cuándo conviene usar shallowMount en lugar de mount? Se recurre a shallowMount cuando un componente renderiza hijos pesados o ya probados y la prueba solo se interesa por la lógica propia del padre. Reemplazar los hijos por stubs aísla la unidad y acelera el renderizado, a cambio de no verificar la integración real entre componentes.

¿Cómo se manejan las actualizaciones asíncronas en Vue Test Utils? Vue aplica los cambios reactivos en el siguiente tick. Ayudantes de eventos como trigger y setValue devuelven promesas que se resuelven después de que el DOM se actualiza, así que basta con esperarlas. Para el estado modificado fuera de esos ayudantes, await nextTick() fuerza el vaciado de la cola antes de que se ejecuten las verificaciones.

¿Cuál es la diferencia entre find y get? find devuelve un wrapper vacío cuando ningún elemento coincide y nunca lanza una excepción, lo que encaja con verificaciones como expect(wrapper.find('.error').exists()).toBe(false). get lanza una excepción de inmediato ante un elemento ausente, lo que lo vuelve la mejor opción cuando se espera que el elemento exista y su ausencia constituye una falla genuina.

¿Por qué preferir atributos data-test frente a selectores de clase? Los nombres de clase cambian con el estilo y la estructura, así que las pruebas que los consultan se rompen ante ediciones cosméticas. Un gancho data-test dedicado expresa la intención de la prueba de forma explícita y sobrevive a las refactorizaciones, reduciendo los falsos negativos.

¿Cómo se prueba un composable que depende de onMounted? Los hooks del ciclo de vida solo se disparan dentro de una instancia de componente, por lo que el composable no puede invocarse directamente. La técnica estándar es un componente auxiliar desechable que invoca el composable en setup y expone su valor de retorno, y luego se monta ese auxiliar con Vue Test Utils y se verifica a través del wrapper. Así se mantiene intacto el cableado de reactividad y ciclo de vida mientras se aísla la lógica bajo prueba. Se recopilan más preguntas conceptuales y de código en la guía de preguntas esenciales de entrevista de Vue.js.

Conclusión

Una configuración fiable de testing en Vue en 2026 se reduce a unas pocas decisiones deliberadas:

  • Ejecutar Vitest con environment: 'jsdom' y el plugin de Vue para que las pruebas compartan exactamente la cadena de transformación de Vite de la aplicación.
  • Preferir mount sobre shallowMount salvo que un componente hijo sea pesado o ya esté cubierto en otro lugar.
  • Esperar siempre con await a trigger, setValue y nextTick antes de verificar, para eliminar las pruebas inestables dependientes del tiempo.
  • Probar los composables que solo usan APIs de reactividad invocándolos directamente y verificando sobre .value.
  • Simular en la frontera del módulo con vi.mock para que el comportamiento de red sea determinista y los caminos de error se puedan probar.
  • Usar createTestingPinia({ createSpy: vi.fn }) para las pruebas de componente y stubActions: false cuando se ejercita la lógica del store.
  • Consultar los elementos mediante atributos data-test para que las pruebas se rompan solo ante cambios reales de comportamiento y nunca por el estilo.

Dominar estos patrones convierte el testing en una herramienta de diseño en vez de una tarea tediosa, atrapando regresiones antes de que lleguen a producción. Se puede explorar la ruta de aprendizaje completa de Vue y Nuxt para seguir construyendo habilidades listas para la entrevista.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

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

Compartir

Artículos relacionados