lobo_tuerto's notes
Home
Blog
Notes
About

Vue 3 testing cheat sheet

What are the basic constructs used for testing in Vue?

📅Published29 July 2022Last updatedSep 2022
🏷️
frontendtestingvue 3

Lately I’ve found that colocated tests are better.
In this way is easier to see which components are missing some, or just have a quick glance at the tests of that component right there! — as opposed to slowly open a test folder and crawl a hierarchy of test files.

mount vs shallowMount

mount will render the whole component and all of its child components.
shallowMount will stub all the child component.

I think it’s preferably to use mount and only stub the things that you need.

Stubs

This is how you can stub sub components:

import { mount } from '@vue/test-utils'
import App from './App.vue'

test('mount component', () => {
  expect(App).toBeTruthy()

  const wrapper = mount(App, {
    global: {
      stubs: {
        MiniMap: true
      }
    }
  })

  expect(wrapper.text()).toContain('MiniMap')
  expect(wrapper.html()).toContain('<mini-map-stub')

  expect(wrapper.html()).toMatchSnapshot()
})

If you pass true to the stub definition it’ll render a component like:
<component-name-stub attr-1="algo" other-attr="otro"></component-name-stub>
so you can test against <component-name-stub or </component-name-stub>.

Why not test against <component-name-stub>?
Because if you are passing props or assigning attributes — like classes, etc. — then you won’t have a match.

Another way is to provide your own stub template and test against that:

import { mount } from '@vue/test-utils'
import App from './App.vue'

test('mount component 2', () => {
  expect(App).toBeTruthy()

  const wrapper = mount(App, {
    global: {
      stubs: {
        MiniMap: {
          template: '<div>|THE MINI MAP|</div>'
        }
      }
    }
  })

  expect(wrapper.text()).toContain('MiniMap')
  expect(wrapper.text()).toContain('|THE MINI MAP|')

  expect(wrapper.html()).toMatchSnapshot()
})

Why not test against <div>|THE MINI MAP|</div>?
For the same reasons described above for <component-name-stub>.

Props and inject

This is how you pass props to a component being tested and also how you can provide a value for injection:

import { mount } from '@vue/test-utils'
import MiniMapControls from './MiniMapControls.vue'

test('toggles control pane', () => {
  const wrapper = mount(MiniMapControls, {
    props: {
      addedTexts: [],
      frameHeight: 400,
      frameWidth: 600,
      layers: []
    },
    global: {
      provide: {
        ppi: 96
      }
    }
  })

  // ...
})

Test click actions and their effects

We’ll need async tests for this:

import { mount } from '@vue/test-utils'
import MiniMapControls from './MiniMapControls.vue'

test('toggles control pane', async () => {
  const wrapper = mount(MiniMapControls, {
    props: {
      addedTexts: [],
      frameHeight: 400,
      frameWidth: 600,
      layers: []
    },
    global: {
      provide: {
        ppi: 96
      }
    }
  })

  expect(wrapper.find('.control-pane').exists()).toBe(false)

  const addIconButton = wrapper.get('button[name="Add icon"]')

  await addIconButton.trigger('click')
  expect(wrapper.find('.control-pane').exists()).toBe(true)

  await addIconButton.trigger('click')
  expect(wrapper.find('.control-pane').exists()).toBe(false)
})

Test with a real router

import { flushPromises, mount } from '@vue/test-utils'

import App from './App.vue'
import { router } from './router'

test('mount component', () => {
  expect(App).toBeTruthy()

  const wrapper = mount(App, {
    global: {
      plugins: [router]
    }
  })

  expect(wrapper.text()).toContain('rev-')
})

test('render landing page', async () => {
  const wrapper = mount(App, {
    global: {
      plugins: [router]
    }
  })

  await router.isReady()

  expect(wrapper.text()).toContain('some text that is mounted on the / path component')
})

test('render 404 page', async () => {
  const wrapper = mount(App, {
    global: {
      plugins: [router]
    }
  })

  router.push('/non-existing')
  await flushPromises()

  expect(wrapper.text()).toContain('404')
})

Test event handling + function calling

We want to test that functions are called —usually in response to event handling.

Note
To have spies being called you'll need to add parentheses to the event handlers.

Instead of having the code like this:

<input @input="onInput" />

<HelloWorld msg="Vite + Vue" @something="someEventHandler" />

You need to write it like this:

<input @input="onInput($event)" />

<HelloWorld msg="Vite + Vue" @something="someEventHandler()" />

Else, when testing with spies it WON’T work.

See previous discussion about this bug:

DOM element example

TextInput.test.ts:

import { mount } from '@vue/test-utils'

import TextInput from './TextInput.vue'

describe('rendering', () => {
  test('component mounts', () => {
    expect(TextInput).toBeTruthy()

    const wrapper = mount(TextInput, {
      props: {
        label: 'Name',
        placeholder: 'Input your name'
      }
    })

    expect(wrapper.text()).toContain('Hello')
    expect(wrapper.html()).toContain('Input your name')
  })

  test('function is called on input events', () => {
    const wrapper = mount(TextInput, {
      props: {
        label: '',
      }
    })

    const inputSpy = vi.spyOn(wrapper.vm, 'setIsFocused')

    const textInput = wrapper.get('input')
    textInput.trigger('focus')
    textInput.trigger('blur')

    expect(inputSpy).toHaveBeenCalledTimes(2)
  })
})

Test CSS classes

Sometimes you need to test for CSS classes because it’s a presentational component that accepts variants, for example:

Divider.test.ts:

import { mount } from '@vue/test-utils'

import Divider from './Divider.vue'

describe('rendering', () => {
  test('exists', () => {
    expect(Divider).toBeTruthy()
  })

  test('main variant', () => {
    const wrapper = mount(Divider, {
      props: {
        variant: 'main'
      }
    })

    expect(wrapper.classes()).toContain('main-variant')
    expect(wrapper.classes()).toContain('bg-border-red-400')
  })
})

Inputs

import { mount } from '@vue/test-utils'

import TextInput from './TextInput.vue'

describe('rendering', () => {
  test('the value is set', () => {
    const wrapper = mount(TextInput, {
      props: {
        modelValue: 'Example value'
      }
    })

    const input = wrapper.find('input')

    expect(input.element.value).toBe('Example value')
  })
})

Components that use stores (Pinia)

pnpm add -D @pinia/testing

If we are using a store in src/App.vue, then in src/App.test.ts:

import { createTestingPinia } from '@pinia/testing'

import { router } from '@/router'

test('rendering', () => {
  expect(App).toBeTruthy()

  const wrapper = mount(App, {
    global: {
      plugins: [router, createTestingPinia()]
    }
  })
})

Misc notes

If you aren’t seeing expected changes in state or the DOM when testing, remember to always use await when calling $nextTick or trigger.

await wrapper.vm.$nextTick()

// Or...

await textInput.trigger('focus')

References


Got comments or feedback?
Follow me on
v-bd1b989