Vue Testing Library
Introduction
Vue Testing Library is a powerful and intuitive testing utility that helps you test Vue components the way users interact with them. Unlike traditional component testing approaches that focus on implementation details, Vue Testing Library encourages testing patterns that simulate how users actually use your application.
Built on top of DOM Testing Library, Vue Testing Library extends its main principles to the Vue.js ecosystem, providing specific helpers for working with Vue components. It focuses on testing what the user sees and interacts with rather than implementation details like component props, state, or methods, leading to more reliable and maintainable tests.
In this guide, we'll explore the fundamentals of Vue Testing Library, how to set it up, and patterns for writing effective tests for your Vue.js applications.
Why Vue Testing Library?
Before diving into the specifics, let's understand why Vue Testing Library stands out compared to traditional testing approaches:
- User-centric testing: It encourages tests that mirror how users interact with your application.
- Implementation-detail independence: Tests don't break when you refactor component internals.
- Accessibility-focused: Promotes testing practices that improve application accessibility.
- Simple API: Provides a clean, intuitive API that's easy to learn and use.
- Works with Jest and other test runners: Seamlessly integrates with popular test environments.
Getting Started with Vue Testing Library
Installation
To begin using Vue Testing Library in your Vue.js project, you'll need to install the necessary packages:
# Using npm
npm install --save-dev @testing-library/vue @testing-library/jest-dom
# Using yarn
yarn add --dev @testing-library/vue @testing-library/jest-dom
Setting Up Your Test Environment
For most projects using Jest, you'll want to setup your test configuration. Create or modify your Jest setup file to include:
// jest-setup.js
import '@testing-library/jest-dom'
And in your Jest configuration (usually in jest.config.js
or your package.json
):
// jest.config.js
module.exports = {
// ... other Jest configurations
setupFilesAfterEnv: ['./jest-setup.js']
}
Basic Testing Concepts
Rendering Components
The core function in Vue Testing Library is render
, which mounts your component and returns utilities to test it:
import { render } from '@testing-library/vue'
import HelloWorld from './HelloWorld.vue'
test('renders welcome message', () => {
// Render the component
const { getByText } = render(HelloWorld, {
props: {
msg: 'Welcome to Vue.js'
}
})
// Assert that the message appears in the document
expect(getByText('Welcome to Vue.js')).toBeInTheDocument()
})
Queries
Vue Testing Library provides several query methods to find elements in your component:
- getBy...: Returns the matching element or throws an error if none or multiple are found
- queryBy...: Returns the matching element or null if none is found
- findBy...: Returns a Promise that resolves when an element is found
Each query comes in different variants:
import { render } from '@testing-library/vue'
import UserProfile from './UserProfile.vue'
test('displays user information', () => {
const { getByText, getByRole, getByTestId } = render(UserProfile, {
props: {
username: 'testuser',
email: '[email protected]'
}
})
// Find by text content
const usernameElement = getByText('Username: testuser')
// Find by ARIA role
const profileSection = getByRole('region', { name: /user profile/i })
// Find by data-testid attribute
const emailField = getByTestId('user-email')
expect(usernameElement).toBeInTheDocument()
expect(profileSection).toBeInTheDocument()
expect(emailField).toHaveTextContent('[email protected]')
})
Simulating User Interactions
Vue Testing Library also provides utility functions to simulate user interactions:
import { render, fireEvent } from '@testing-library/vue'
import Counter from './Counter.vue'
test('increments count when button is clicked', async () => {
// Render the component
const { getByText } = render(Counter)
// Get elements
const button = getByText('Increment')
// Initial state check
expect(getByText('Count: 0')).toBeInTheDocument()
// Simulate user clicking the button
await fireEvent.click(button)
// Check that the count has been updated
expect(getByText('Count: 1')).toBeInTheDocument()
})
Practical Examples
Let's explore some real-world examples of testing Vue components using Vue Testing Library.
Example 1: Testing a Form Component
Consider a simple login form component:
<!-- LoginForm.vue -->
<template>
<form @submit.prevent="submitForm">
<div>
<label for="username">Username:</label>
<input id="username" v-model="username" data-testid="username-input" />
</div>
<div>
<label for="password">Password:</label>
<input
id="password"
type="password"
v-model="password"
data-testid="password-input"
/>
</div>
<div v-if="error" data-testid="error-message" class="error">
{{ error }}
</div>
<button type="submit">Login</button>
</form>
</template>
<script>
export default {
data() {
return {
username: '',
password: '',
error: ''
}
},
methods: {
submitForm() {
if (!this.username || !this.password) {
this.error = 'Please fill in all fields'
return
}
this.error = ''
this.$emit('login', {
username: this.username,
password: this.password
})
}
}
}
</script>
Here's how we'd test this form using Vue Testing Library:
import { render, fireEvent } from '@testing-library/vue'
import LoginForm from './LoginForm.vue'
describe('LoginForm', () => {
test('shows validation error when fields are empty', async () => {
const { getByText, getByTestId } = render(LoginForm)
// Submit the form without filling fields
await fireEvent.click(getByText('Login'))
// Check if error message appears
expect(getByTestId('error-message')).toHaveTextContent('Please fill in all fields')
})
test('emits login event with user credentials when form is valid', async () => {
const { getByLabelText, getByText, emitted } = render(LoginForm)
// Fill the form
await fireEvent.update(getByLabelText('Username:'), 'testuser')
await fireEvent.update(getByLabelText('Password:'), 'password123')
// Submit the form
await fireEvent.click(getByText('Login'))
// Assert that the login event was emitted with correct data
expect(emitted()).toHaveProperty('login')
expect(emitted().login[0][0]).toEqual({
username: 'testuser',
password: 'password123'
})
})
})
Example 2: Testing Asynchronous Operations
Let's test a component that fetches user data from an API:
<!-- UserDetails.vue -->
<template>
<div>
<p v-if="loading">Loading user data...</p>
<div v-else-if="user" data-testid="user-container">
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<p>Phone: {{ user.phone }}</p>
</div>
<p v-else-if="error" data-testid="error-container" class="error">
{{ error }}
</p>
<button @click="fetchUserData" data-testid="fetch-button">
Fetch User Data
</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
user: null,
loading: false,
error: null
}
},
methods: {
async fetchUserData() {
this.loading = true
this.error = null
try {
const response = await axios.get('https://api.example.com/users/1')
this.user = response.data
} catch (err) {
this.error = 'Failed to load user data'
} finally {
this.loading = false
}
}
}
}
</script>
Here's how we'd test this component:
import { render, fireEvent, waitFor } from '@testing-library/vue'
import UserDetails from './UserDetails.vue'
import axios from 'axios'
// Mock axios
jest.mock('axios')
describe('UserDetails', () => {
test('shows loading state and then user data when fetch succeeds', async () => {
// Mock axios.get to return sample data
axios.get.mockResolvedValueOnce({
data: {
name: 'John Doe',
email: '[email protected]',
phone: '555-1234'
}
})
const { getByTestId, getByText, queryByText } = render(UserDetails)
// Click the fetch button
await fireEvent.click(getByTestId('fetch-button'))
// Check loading state
expect(getByText('Loading user data...')).toBeInTheDocument()
// Wait for user data to be displayed
await waitFor(() => {
expect(queryByText('Loading user data...')).not.toBeInTheDocument()
expect(getByTestId('user-container')).toBeInTheDocument()
})
// Verify the user data is displayed correctly
expect(getByText('John Doe')).toBeInTheDocument()
expect(getByText('Email: [email protected]')).toBeInTheDocument()
expect(getByText('Phone: 555-1234')).toBeInTheDocument()
})
test('shows error message when fetch fails', async () => {
// Mock axios.get to reject
axios.get.mockRejectedValueOnce(new Error('API Error'))
const { getByTestId, getByText, queryByText } = render(UserDetails)
// Click the fetch button
await fireEvent.click(getByTestId('fetch-button'))
// Wait for error message to be displayed
await waitFor(() => {
expect(queryByText('Loading user data...')).not.toBeInTheDocument()
expect(getByTestId('error-container')).toBeInTheDocument()
})
// Verify the error message
expect(getByText('Failed to load user data')).toBeInTheDocument()
})
})
Testing Vuex and Vue Router Integration
Testing Components with Vuex
Vue Testing Library makes it easy to test components that interact with Vuex:
import { render } from '@testing-library/vue'
import { createStore } from 'vuex'
import TodoList from './TodoList.vue'
test('renders todos from the Vuex store', () => {
// Create a store with initial state
const store = createStore({
state: {
todos: [
{ id: 1, text: 'Learn Vue', done: true },
{ id: 2, text: 'Learn Testing Library', done: false }
]
},
getters: {
allTodos: state => state.todos
}
})
// Render the component with the store
const { getByText } = render(TodoList, {
global: {
plugins: [store]
}
})
// Assert that todos from the store are displayed
expect(getByText('Learn Vue')).toBeInTheDocument()
expect(getByText('Learn Testing Library')).toBeInTheDocument()
})
Testing Components with Vue Router
For components that use Vue Router, we can similarly mock the router in our tests:
import { render } from '@testing-library/vue'
import { createRouter, createWebHistory } from 'vue-router'
import NavBar from './NavBar.vue'
test('renders navigation links', () => {
// Create a router with routes
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', name: 'Home', component: { template: '<div>Home</div>' } },
{ path: '/about', name: 'About', component: { template: '<div>About</div>' } }
]
})
// Render the component with the router
const { getAllByRole } = render(NavBar, {
global: {
plugins: [router]
}
})
// Get all navigation links
const navLinks = getAllByRole('link')
// Assert that links are rendered with correct hrefs
expect(navLinks[0]).toHaveTextContent('Home')
expect(navLinks[0]).toHaveAttribute('href', '/')
expect(navLinks[1]).toHaveTextContent('About')
expect(navLinks[1]).toHaveAttribute('href', '/about')
})
Best Practices
To get the most out of Vue Testing Library, consider these best practices:
-
Test user behavior, not implementation details:
javascript// Good: Tests what the user sees
test('shows welcome message', () => {
const { getByText } = render(App)
expect(getByText('Welcome to Vue.js')).toBeInTheDocument()
})
// Avoid: Tests implementation details
test('sets the message data property', () => {
const wrapper = shallowMount(App)
expect(wrapper.vm.message).toBe('Welcome to Vue.js')
}) -
Use accessible queries whenever possible:
javascript// Good: Uses accessible queries
const submitButton = getByRole('button', { name: 'Submit' })
// Avoid: Relies on non-accessible attributes
const submitButton = getByTestId('submit-button') -
Test for accessibility:
javascripttest('form is accessible', () => {
const { getByLabelText } = render(Form)
// Ensure form controls have associated labels
expect(getByLabelText('Username')).toBeInTheDocument()
expect(getByLabelText('Password')).toBeInTheDocument()
}) -
Use data-testid as a last resort:
javascript// Only use data-testid when other queries won't work
const userProfileSection = getByTestId('user-profile') -
Structure tests according to user stories:
javascriptdescribe('Login form', () => {
test('allows a user to login with valid credentials', async () => {
// Test successful login flow
})
test('shows an error message with invalid credentials', async () => {
// Test unsuccessful login flow
})
})
Testing Patterns and Anti-patterns
Good Patterns
// ✅ Testing user interactions
test('counter increments when increment button is clicked', async () => {
const { getByRole, getByText } = render(Counter)
expect(getByText('Count: 0')).toBeInTheDocument()
await fireEvent.click(getByRole('button', { name: 'Increment' }))
expect(getByText('Count: 1')).toBeInTheDocument()
})
// ✅ Testing accessibility
test('all images have alt text', () => {
const { getAllByRole } = render(ImageGallery)
const images = getAllByRole('img')
images.forEach(img => {
expect(img).toHaveAttribute('alt')
})
})
Anti-patterns
// ❌ Testing implementation details
test('toggles the isOpen variable', async () => {
const wrapper = mount(Dropdown)
expect(wrapper.vm.isOpen).toBe(false)
await wrapper.find('button').trigger('click')
expect(wrapper.vm.isOpen).toBe(true)
})
// ❌ Using querySelector instead of Testing Library queries
test('renders links', () => {
const { container } = render(NavBar)
const links = container.querySelectorAll('a')
expect(links.length).toBe(3)
})
Summary
Vue Testing Library is a powerful ally for writing maintainable tests that focus on user behavior rather than implementation details. By following the guiding principles of this library, we can create tests that:
- Verify our application works as users expect
- Don't break when we refactor internals
- Help ensure accessibility
- Provide confidence in our application's functionality
The key concepts we've covered include:
- Rendering components with the
render
function - Finding elements using queries like
getByText
,getByRole
, etc. - Simulating user interactions with
fireEvent
- Testing asynchronous operations with
waitFor
andfindBy
queries - Integrating with Vuex and Vue Router for comprehensive tests
Remember that good tests act like users—clicking buttons, filling forms, and reading content—rather than checking internal component state or methods.
Additional Resources
To deepen your understanding of Vue Testing Library, consider these resources:
- Vue Testing Library Official Documentation
- DOM Testing Library Cheatsheet
- Common Mistakes with React Testing Library - Many concepts apply to Vue Testing Library as well
- Testing Library Query Priority Guide
Practice Exercises
-
Create a simple TodoList component with add, remove, and toggle completion functionality, then write comprehensive tests using Vue Testing Library.
-
Take an existing component in your project and refactor its tests to use Vue Testing Library, focusing on user behavior.
-
Write tests for a form component that validates user input and displays error messages.
-
Test a component that loads data asynchronously, handling loading, success, and error states.
-
Create a test that verifies a component is accessible, checking for proper labels, ARIA attributes, and focus management.
By applying the principles and techniques covered in this guide, you'll be well-equipped to write effective and maintainable tests for your Vue.js applications using Vue Testing Library.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)