Vue Test Utils

repository·main·Indexed 22 days ago

https://github.com/vuejs/test-utils

A testing utility library for Vue.js (version 2.4.11) providing tools for mounting components, inspecting state, and simulating user interactions. It includes functions like mount() and shallowMount() for creating VueWrappers, as well as utilities for finding elements, triggering events, and managing asynchronous DOM updates via flushPromises and nextTick.

Tokens
31.7K
Snippets
131
Records
147
Agent score
74%

What's inside @vue/test-utils

  1. What is Vue Test Utils?

    main

    Vue Test Utils (VTU) is the official utility library for testing Vue.js components. It provides methods to mount and interact with Vue components in an isolated manner, simplifying the testing process.

    Note that this documentation refers to Vue Test Utils v2, which is designed for Vue 3. If you are using Vue 2, you should use Vue Test Utils v1.

  2. Summary of element finding strategies

    main

    Choose your method based on the intended test outcome:

    GoalMethodBehavior
    Assert presenceget(selector)Throws error if element is missing.
    Assert absencefind(selector).exists()Returns false if element is missing; does not throw.
    Assert visibilityget(selector).isVisible()Checks if element is in DOM AND visible (not hidden by CSS).

    Note: isVisible() requires attachTo: document.body to work correctly with v-show.

  3. Write a Vue Wrapper Plugin

    main

    A Vue Test Utils plugin is a function that receives the mounted VueWrapper or DOMWrapper instance and returns an object containing the new properties or methods you wish to attach to that instance.

    Common use cases include:

    • Aliasing existing public methods.
    • Attaching custom matchers.
    • Adding helper methods (e.g., finding elements by data-testid).
    import { config, DOMWrapper } from '@vue/test-utils'
    
    // A plugin that adds a 'findByTestId' method to the wrapper
    const DataTestIdPlugin = wrapper => {
      function findByTestId(selector) {
        const dataSelector = `[data-testid='${selector}']`
        const element = wrapper.element.querySelector(dataSelector)
        return new DOMWrapper(element)
      }
    
      return {
        findByTestId
      }
    }
    
    config.plugins.VueWrapper.install(DataTestIdPlugin)
  4. Test scoped slots and access slot scope via `params`

    main

    Vue Test Utils supports scoped slots and the # shorthand. When testing scoped slots, you have two ways to access the slot's scope (the data bound to the slot):

    1. Explicit Template: Use a wrapping <template #slotName="scopeVar"> tag within your slot string. This allows you to use scopeVar to access bound data.
    2. Implicit params: If you provide a raw string without a wrapping <template> tag, the slot scope is automatically exposed via a special params object. You can access bound data using {{ params.propertyName }}.
    // Method 1: Explicit template tag
    const wrapper = mount(ComponentWithSlots, {
      slots: {
        scoped: `<template #scoped="scope">
          Hello {{ scope.msg }}
        </template>`
      }
    })
    
    // Method 2: Using the implicit 'params' object
    const wrapper = mount(ComponentWithSlots, {
      slots: {
        scoped: `Hello {{ params.msg }}`
      }
    })
  5. Choosing between mount, shallow, and stubs

    main

    Deciding which mounting strategy to use depends on the goal of your test:

    • mount: Renders the entire component hierarchy. This is the most production-like approach and provides the highest confidence as it resembles real browser usage.
    • shallow: Automatically stubs all child components. Use this for testing complex components in complete isolation.
    • mount + stubs: A middle ground. If you only need to isolate one or two specific components, use mount and manually stub only those. This keeps the test more realistic than a full shallow render.

    Best Practice: Focus on inputs (props, user interactions via trigger) and outputs (rendered DOM, emitted events) rather than implementation details.

  6. How transitions are handled in tests

    main

    By default, @vue/test-utils mocks (stubs) <transition> and <transition-group> components. This allows you to test the resulting DOM state immediately without waiting for CSS transitions to complete, making tests faster and more deterministic. You can test components containing transitions as if they were standard components, checking for the presence or absence of elements after triggers (like clicks) that toggle visibility.

    import Component from './Component.vue'
    import { mount } from '@vue/test-utils'
    
    test('works with transitions', async () => {
      const wrapper = mount(Component)
    
      // Initial state: element does not exist
      expect(wrapper.find('p').exists()).toBe(false)
    
      // Trigger change
      await wrapper.find('button').trigger('click')
    
      // Post-transition state: element exists and contains expected text
      expect(wrapper.get('p').text()).toEqual('hello')
    })
  7. Follow the Arrange, Act, Assert pattern

    main

    Effective tests typically follow three distinct phases:

    1. Arrange: Set up the initial state and scenario (e.g., mounting the component, providing props, or setting up a store).
    2. Act: Perform the actions that trigger the behavior you want to test (e.g., clicking a button, typing in an input, or triggering an event).
    3. Assert: Verify that the resulting state or DOM matches your expectations (e.g., checking if a new item appeared in a list or a class was applied).
    test('example test', async () => {
      // Arrange
      const wrapper = mount(TodoApp)
    
      // Act
      await wrapper.get('[data-test="new-todo"]').setValue('New todo')
      await wrapper.get('[data-test="form"]').trigger('submit')
    
      // Assert
      expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
    })
    test('creates a todo', async () => {
      const wrapper = mount(TodoApp)
    
      await wrapper.get('[data-test="new-todo"]').setValue('New todo')
      await wrapper.get('[data-test="form"]').trigger('submit')
    
      expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
    })
  8. Avoid testing implementation details

    main

    To write meaningful and maintainable tests, focus on the component from a user's perspective by testing Inputs and Outputs rather than internal implementation details.

    Inputs

    • Interactions: Human actions like clicking or typing.
    • Props: Arguments passed into the component.
    • Data streams: Incoming data from API calls or subscriptions.

    Outputs

    • DOM elements: Observable nodes rendered to the document.
    • Events: Emitted events via $emit.
    • Side Effects: Actions like console.log or API calls.

    Rule of Thumb: A test should not break during a refactor if the external behavior remains unchanged. If a test fails because you renamed an internal variable or a CSS class (while the component still functions correctly), you are likely testing implementation details, which leads to false positives.

    import { mount } from '@vue/test-utils'
    
    test('text updates on clicking', async () => {
      const wrapper = mount(Counter)
    
      // Testing output via text content rather than internal data
      expect(wrapper.text()).toContain('Times clicked: 0')
    
      // Testing via user interaction (Input)
      const button = wrapper.find('button')
      await button.trigger('click')
      await button.trigger('click')
    
      expect(wrapper.text()).toContain('Times clicked: 2')
    })
  9. Pass extra data to trigger() for complex events

    main

    If your component's event handler relies on properties within the event object (e.g., event.relatedTarget), you can pass an object as the second argument to trigger() to mock those properties.

    Example:

    // Simulating a blur event where the focus moved to a specific button
    const componentToGetFocus = wrapper.find('button')
    
    await wrapper.find('input').trigger('blur', {
      relatedTarget: componentToGetFocus.element
    })
    import Form from './Form.vue'
    
    test('emits an event only if you lose focus to a button', () => {
      const wrapper = mount(Form)
    
      const componentToGetFocus = wrapper.find('button')
    
      wrapper.find('input').trigger('blur', {
        relatedTarget: componentToGetFocus.element
      })
    
      expect(wrapper.emitted('focus-lost')).toBeTruthy()
    })
  10. Use a real Vue Router instance with Composition API

    main

    To test components with a real router instance, instantiate a new router object for each test using createRouter and createWebHistory. This prevents state leakage between tests.

    When using a real router, you must:

    1. Register the router instance in the global.plugins option of mount.
    2. Use await router.isReady() to ensure the router is fully initialized before proceeding.
    3. Use jest.spyOn(router, 'push') (or your runner's equivalent) to assert that navigation occurred.

    Note that Vue Router 4 is asynchronous, so ensure your test handles the router's lifecycle correctly.

    import { mount } from '@vue/test-utils'
    import { createRouter, createWebHistory } from 'vue-router'
    import { routes } from '@/router'
    
    let router
    
    beforeEach(async () => {
      router = createRouter({
        history: createWebHistory(),
        routes: routes
      })
    
      router.push('/')
      await router.isReady()
    })
    
    test('allows authenticated user to edit a post', async () => {
      const wrapper = mount(Component, {
        props: { isAuthenticated: true },
        global: {
          plugins: [router]
        }
      })
    
      const push = jest.spyOn(router, 'push')
      await wrapper.find('button').trigger('click')
    
      expect(push).toHaveBeenCalledWith('/posts/1/edit')
    })
  11. Test components using `useStore` with an injection key

    main

    When useStore is used with a unique injection key (e.g., a Symbol), you must provide the store using that exact key. You can do this in two ways:

    1. Using global.provide: Pass the key as a computed property name in the provide object.
    2. Using global.plugins: Pass an array containing both the store and the key [store, key] to the plugins array.
    // Option 1: global.provide
    import { key } from './store'
    
    const wrapper = mount(App, {
      global: {
        provide: {
          [key]: store
        }
      }
    })
    
    // Option 2: global.plugins
    const wrapper = mount(App, {
      global: {
        plugins: [[store, key]]
      }
    })