What are the basic constructs used for testing in Vue?
Lately I’ve found that co-located 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
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.
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>
.
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
}
}
})
// ...
})
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)
})
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')
})
We want to test that functions are called —usually in response to event handling.
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:
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)
})
})
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')
})
})
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')
})
})
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()]
}
})
})
If you followed the guide for setting up a new Vue + Vite app. Then you already added the c8
dependency for code coverage.
In vite.config.ts
:
export default defineConfig(({ mode }) => {
return {
test: {
coverage: {
skipFull: true,
branches: 60,
functions: 60,
lines: 60,
statements: 60,
}
}
}
})
In vite.config.ts
:
export default defineConfig(({ mode }) => {
return {
test: {
setupFiles: ['./test/setup.ts']
}
}
})
Here we’ll need to mock the matchMedia
window property.
In test/setup.ts
:
import { vi } from 'vitest'
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => {
return {
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}
}),
})
Here we’ll need to mock the Axios instance.
In test/setup.ts
:
import { vi } from 'vitest'
vi.mock('axios', () => {
return {
// This is because it uses a default export
default: {
create() {
return {
get() {
return Promise.resolve({})
},
post: vi.fn(),
interceptors: {
request: {
use: vi.fn(),
},
response: {
use: vi.fn(),
},
},
}
},
},
}
})
Let’s say you have a data service (for users) in a component you are testing; you could mock it like this:
vi.mock('@/services/data/userService', () => {
return {
// This is because it uses a named export
userService: {
me: () => Promise.resolve({ id: 1, email: 'user@email.com' }),
},
}
})
If this service needs to be mocked in several tests, you can put it in your test/setup.ts
file.
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')