How to test Vue components with Jest
Testing Vue components is essential for maintaining code quality and preventing regressions as your application grows and evolves. With over 10 years of experience building Vue applications since 2014 and as the creator of CoreUI, a widely used open-source UI library, I’ve written thousands of component tests in production environments. The most effective approach is to use Jest with Vue Test Utils, which provides a comprehensive testing framework with excellent Vue integration. This combination offers fast test execution, powerful matchers, and intuitive component mounting and interaction APIs.
Use Jest with @vue/test-utils to mount components, interact with them, and assert their behavior in a test environment.
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
describe('HelloWorld', () => {
it('renders the message prop', () => {
const wrapper = mount(HelloWorld, {
props: {
message: 'Hello Jest'
}
})
expect(wrapper.text()).toContain('Hello Jest')
})
})
This basic test mounts the HelloWorld component with a message prop and verifies that the text appears in the rendered output. The mount() function creates a wrapper around the component, providing methods to query and interact with it. The text() method returns all text content, and toContain() checks if the expected message is present.
Setting Up Jest for Vue 3
{
"jest": {
"testEnvironment": "jsdom",
"transform": {
"^.+\\.vue$": "@vue/vue3-jest",
"^.+\\.js$": "babel-jest"
},
"moduleFileExtensions": ["js", "json", "vue"],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"testMatch": [
"**/tests/**/*.spec.js",
"**/__tests__/**/*.js"
],
"collectCoverageFrom": [
"src/**/*.{js,vue}",
"!src/main.js",
"!**/node_modules/**"
]
}
}
The Jest configuration sets up the testing environment for Vue 3 components. The jsdom environment simulates a browser environment in Node.js, allowing DOM operations. The transform configuration tells Jest to use @vue/vue3-jest for Vue files and babel-jest for JavaScript files. The moduleNameMapper handles path aliases like @/components, and testMatch specifies where test files are located. This configuration should be added to your package.json or a separate jest.config.js file.
Testing Component Props
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
describe('UserCard', () => {
it('displays user name and email', () => {
const user = {
name: 'John Doe',
email: '[email protected]',
role: 'Admin'
}
const wrapper = mount(UserCard, {
props: { user }
})
expect(wrapper.find('.user-name').text()).toBe('John Doe')
expect(wrapper.find('.user-email').text()).toBe('[email protected]')
expect(wrapper.find('.user-role').text()).toBe('Admin')
})
it('shows default values when no user provided', () => {
const wrapper = mount(UserCard, {
props: {
user: {
name: '',
email: ''
}
}
})
expect(wrapper.find('.user-name').text()).toBe('Unknown User')
})
it('validates required props', () => {
expect(() => {
mount(UserCard)
}).toThrow()
})
})
These tests verify that the component correctly displays prop data in the template. The find() method selects elements by CSS selector, and text() retrieves their content. Testing default values ensures the component handles missing or incomplete data gracefully. The third test checks that required props throw errors when missing, validating your prop definitions.
Testing User Interactions and Events
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('increments count when button clicked', async () => {
const wrapper = mount(Counter)
expect(wrapper.find('.count').text()).toBe('0')
await wrapper.find('.increment-btn').trigger('click')
expect(wrapper.find('.count').text()).toBe('1')
})
it('emits update event with new value', async () => {
const wrapper = mount(Counter)
await wrapper.find('.increment-btn').trigger('click')
expect(wrapper.emitted()).toHaveProperty('update')
expect(wrapper.emitted('update')[0]).toEqual([1])
})
it('handles multiple clicks correctly', async () => {
const wrapper = mount(Counter)
await wrapper.find('.increment-btn').trigger('click')
await wrapper.find('.increment-btn').trigger('click')
await wrapper.find('.increment-btn').trigger('click')
expect(wrapper.find('.count').text()).toBe('3')
expect(wrapper.emitted('update')).toHaveLength(3)
})
})
Testing interactions involves triggering events on elements and verifying the resulting behavior. The trigger() method simulates user actions like clicks, input, and keyboard events. The await keyword is crucial because Vue updates the DOM asynchronously. The emitted() method returns all events emitted by the component, allowing you to verify event names, frequencies, and payload values.
Testing Form Inputs
import { mount } from '@vue/test-utils'
import LoginForm from './LoginForm.vue'
describe('LoginForm', () => {
it('updates email and password on input', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('input[type="email"]').setValue('[email protected]')
await wrapper.find('input[type="password"]').setValue('password123')
expect(wrapper.vm.email).toBe('[email protected]')
expect(wrapper.vm.password).toBe('password123')
})
it('submits form with correct data', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('input[type="email"]').setValue('[email protected]')
await wrapper.find('input[type="password"]').setValue('password123')
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.emitted('submit')[0]).toEqual([{
email: '[email protected]',
password: 'password123'
}])
})
it('shows validation errors for invalid input', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('input[type="email"]').setValue('invalid-email')
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.find('.error-message').exists()).toBe(true)
expect(wrapper.find('.error-message').text()).toContain('Invalid email')
})
})
Form testing requires setting input values and verifying that the component state updates accordingly. The setValue() method simulates user input, updating both the input element and any v-model bindings. The vm property accesses the component instance, allowing direct inspection of data properties. Testing validation ensures error messages appear when users submit invalid data.
Testing Async Operations
import { mount } from '@vue/test-utils'
import UserList from './UserList.vue'
describe('UserList', () => {
it('displays loading state while fetching', () => {
const wrapper = mount(UserList)
expect(wrapper.find('.loading').exists()).toBe(true)
expect(wrapper.find('.users-list').exists()).toBe(false)
})
it('displays users after successful fetch', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
])
})
)
const wrapper = mount(UserList)
await wrapper.vm.$nextTick()
await new Promise(resolve => setTimeout(resolve, 0))
expect(wrapper.find('.loading').exists()).toBe(false)
expect(wrapper.findAll('.user-item')).toHaveLength(2)
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('Jane Smith')
})
it('displays error message on fetch failure', async () => {
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network error'))
)
const wrapper = mount(UserList)
await wrapper.vm.$nextTick()
await new Promise(resolve => setTimeout(resolve, 0))
expect(wrapper.find('.error-message').exists()).toBe(true)
expect(wrapper.find('.error-message').text()).toContain('Error')
})
})
Testing async operations requires mocking external dependencies like fetch or axios. Jest’s jest.fn() creates a mock function that returns predetermined values. Multiple await statements ensure all promises resolve and Vue updates the DOM. The $nextTick() method waits for the next DOM update cycle, and the timeout ensures all microtasks complete. This pattern handles components that fetch data on mount or in response to user actions.
Testing Computed Properties and Watchers
import { mount } from '@vue/test-utils'
import PriceCalculator from './PriceCalculator.vue'
describe('PriceCalculator', () => {
it('calculates total price correctly', async () => {
const wrapper = mount(PriceCalculator)
await wrapper.find('.quantity-input').setValue(3)
await wrapper.find('.price-input').setValue(10)
expect(wrapper.vm.totalPrice).toBe(30)
expect(wrapper.find('.total').text()).toContain('30')
})
it('applies discount when applicable', async () => {
const wrapper = mount(PriceCalculator)
await wrapper.find('.quantity-input').setValue(10)
await wrapper.find('.price-input').setValue(100)
expect(wrapper.vm.discount).toBe(100)
expect(wrapper.vm.finalPrice).toBe(900)
})
it('triggers watcher when price changes', async () => {
const wrapper = mount(PriceCalculator)
const spy = jest.spyOn(wrapper.vm, 'onPriceChange')
await wrapper.find('.price-input').setValue(50)
expect(spy).toHaveBeenCalledWith(50, 0)
})
})
Computed properties are tested by setting their dependencies and verifying the computed result. Access computed values through wrapper.vm.computedPropertyName. Watchers can be tested by spying on the watcher function using jest.spyOn(), then triggering the change and verifying the watcher was called with the correct arguments. This ensures your reactive logic works correctly.
Mocking Child Components
import { mount } from '@vue/test-utils'
import ParentComponent from './ParentComponent.vue'
import ChildComponent from './ChildComponent.vue'
describe('ParentComponent', () => {
it('passes correct props to child component', () => {
const wrapper = mount(ParentComponent, {
global: {
stubs: {
ChildComponent: {
template: '<div class="child-stub">{{ title }}</div>',
props: ['title']
}
}
}
})
const child = wrapper.findComponent({ name: 'ChildComponent' })
expect(child.props('title')).toBe('Expected Title')
})
it('handles child component events', async () => {
const wrapper = mount(ParentComponent, {
global: {
components: { ChildComponent }
}
})
const child = wrapper.findComponent(ChildComponent)
await child.vm.$emit('update', 'new value')
expect(wrapper.vm.dataFromChild).toBe('new value')
})
})
Stubbing child components prevents their logic from running during parent tests, isolating the test to the parent’s behavior. The stubs option replaces child components with simple templates. The findComponent() method locates child component wrappers, allowing you to verify props passed to children or trigger events from them. This approach speeds up tests and reduces coupling between components.
Best Practice Note
For comprehensive testing strategies including unit tests, integration tests, and E2E tests, maintain at least 80% code coverage for critical components. Always test user-facing behavior rather than implementation details to make tests resilient to refactoring. For more testing guidance, check out how to run unit tests in Vue and how to test Vue components with Vue Test Utils. This is the same testing approach we use in CoreUI components to ensure reliability and maintainability across all Vue applications.



