We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
2022 / 07 / 29
2023 / 01 / 29
Vue 3 testing cheat sheet
What are the basic constructs used for testing in Vue?
Lately, I’ve been realizing that co-located tests are better.
It’s easier to see which components are tested or not, or quickly open a component’s tests right there! — as opposed to going to a different folder to 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.
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()]
}
})
})
Set test coverage thresholds
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,
}
}
}
})
Add a test setup file
In vite.config.ts
:
export default defineConfig(({ mode }) => {
return {
test: {
setupFiles: ['./test/setup.ts']
}
}
})
Mocks
window.matchMedia
Here we’ll need to mock the matchMedia
window property.
In test/setup.ts
:
import { vi } from 'vitest'
// Global objects and properties
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()
}
}),
})
axios
vi.mock('axios', () => {
return {
default: {
create() {
return {
post: vi.fn(),
get() {
return Promise.resolve({})
},
interceptors: {
request: {
use: vi.fn()
},
response: {
use: vi.fn()
}
}
}
}
}
}
})
Data services
// Data services
vi.mock('@/services/data/baseService', () => {
return {
createBaseService: () => ({}),
paginationParams: vi.fn(),
baseService: {
get: () => Promise.resolve({ data: [] })
}
}
})
vi.mock('@/services/data/userService', () => {
return {
userService: {
list: () => Promise.resolve({ data: [] }),
me: () => Promise.resolve({ data: { email: 'my@email' } })
}
}
})
Data stores
// Data stores
vi.mock('@/stores/authStore', () => {
return {
useAuthStore() {
return {
maybeLoggedIn: () => Promise.resolve(true)
}
}
}
})
Teleport target in tests
In test/setup.ts
:
// Setup Teleport target for tests
beforeAll(() => {
const el = document.createElement('div')
el.id = 'action-bar'
document.body.append(el)
})
Components that use Axios
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(),
},
},
}
},
},
}
})
Mock imports
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.
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')