Vue.js Mocking
Introduction
When testing Vue.js applications, you'll often encounter scenarios where components depend on external services, APIs, or other components. Testing these dependencies directly can make your tests slow, unreliable, or unnecessarily complex. This is where mocking comes in.
Mocking is the process of creating substitute implementations for dependencies to isolate the code being tested. In Vue.js testing, we commonly mock:
- API calls
- Vuex store modules
- Child components
- Browser APIs
- Third-party libraries
In this guide, we'll explore various mocking techniques in Vue.js using Vue Test Utils and Jest, the most popular testing tools in the Vue ecosystem.
Why Use Mocking?
Before diving into the "how," let's understand the "why":
- Isolation: Test components in isolation from their dependencies
- Speed: Avoid slow operations like API calls or database queries
- Reliability: Prevent tests from failing due to external factors
- Control: Simulate various scenarios including edge cases and errors
Basic Mocking Techniques
Mocking Child Components
When testing a parent component, you often don't need the full implementation of child components. Vue Test Utils provides a stubs
option to replace child components with simplified versions.
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent.vue'
import ComplexChild from '@/components/ComplexChild.vue'
test('parent component renders with stubbed child', () => {
const wrapper = mount(ParentComponent, {
stubs: {
// Replace ComplexChild with a stub
ComplexChild: true
}
})
// The test now focuses on ParentComponent behavior
// without worrying about ComplexChild's implementation
expect(wrapper.html()).toContain('Parent Component Content')
})
You can also provide a custom implementation of the stub:
const wrapper = mount(ParentComponent, {
stubs: {
ComplexChild: {
template: '<div class="stubbed-child">Stubbed Content</div>',
methods: {
someMethod: () => 'stubbed result'
}
}
}
})
Mocking Props and Events
When testing components that pass props to children or listen for events, you can mock these interactions:
import { mount } from '@vue/test-utils'
import UserProfile from '@/components/UserProfile.vue'
test('emits update event when save button is clicked', async () => {
const wrapper = mount(UserProfile, {
props: {
// Mock input props
user: {
id: 1,
name: 'Test User'
}
}
})
// Trigger the save button
await wrapper.find('button.save').trigger('click')
// Check if the correct event was emitted
expect(wrapper.emitted('update')).toBeTruthy()
expect(wrapper.emitted('update')[0][0]).toEqual({ id: 1, name: 'Test User' })
})
Advanced Mocking Techniques
Mocking API Calls with Jest
Most Vue applications make HTTP requests to external APIs. We can use Jest to mock these requests:
Example: Mocking Axios
// UserService.js
import axios from 'axios'
export default {
async getUsers() {
const response = await axios.get('/api/users')
return response.data
}
}
Testing the service with mocked axios:
import UserService from '@/services/UserService'
import axios from 'axios'
// Mock the entire axios module
jest.mock('axios')
test('getUsers returns data from API', async () => {
// Setup mock response
const mockUsers = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
axios.get.mockResolvedValue({ data: mockUsers })
// Call the method
const result = await UserService.getUsers()
// Assert on the results
expect(axios.get).toHaveBeenCalledWith('/api/users')
expect(result).toEqual(mockUsers)
})
Using Mock Service Worker (MSW)
For more complex API mocking scenarios, you might want to use Mock Service Worker (MSW) which allows you to intercept network requests at the network level:
// mocks/handlers.js
import { rest } from 'msw'
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
])
)
}),
rest.post('/api/users', (req, res, ctx) => {
const { name } = req.body
return res(
ctx.status(201),
ctx.json({ id: Date.now(), name })
)
})
]
Then set up the MSW server in your test setup file.
Mocking Vuex Store
When testing components that use Vuex, you often want to provide a mocked store:
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import UserList from '@/components/UserList.vue'
test('displays users from store', () => {
// Create a mock store
const store = createStore({
state: {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
},
getters: {
userCount: state => state.users.length
},
actions: {
fetchUsers: jest.fn()
}
})
// Mount with the mock store
const wrapper = mount(UserList, {
global: {
plugins: [store]
}
})
// Assert component renders correctly
expect(wrapper.findAll('.user-item')).toHaveLength(2)
expect(wrapper.find('.user-count').text()).toBe('Users: 2')
})
Mocking Global Properties and Plugins
Vue 3 applications often use global properties or plugins. You can mock these in your tests:
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
test('uses global $filters', () => {
const wrapper = mount(MyComponent, {
global: {
mocks: {
$filters: {
capitalize: jest.fn(str => str.toUpperCase())
}
}
}
})
// Now the component will use the mocked $filters.capitalize
})
Real-World Example: Testing a User Dashboard
Let's look at a complete example of testing a UserDashboard component that fetches and displays user data:
<!-- UserDashboard.vue -->
<template>
<div class="dashboard">
<h1>User Dashboard</h1>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<user-stats :total-users="users.length" />
<user-list :users="users" @select-user="selectUser" />
<user-details v-if="selectedUser" :user="selectedUser" />
</div>
</div>
</template>
<script>
import UserService from '@/services/UserService'
import UserStats from '@/components/UserStats.vue'
import UserList from '@/components/UserList.vue'
import UserDetails from '@/components/UserDetails.vue'
export default {
components: {
UserStats,
UserList,
UserDetails
},
data() {
return {
users: [],
selectedUser: null,
loading: true,
error: null
}
},
async created() {
try {
this.users = await UserService.getUsers()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
},
methods: {
selectUser(user) {
this.selectedUser = user
}
}
}
</script>
Here's how we'd test this component with various mocking techniques:
import { mount, flushPromises } from '@vue/test-utils'
import UserDashboard from '@/components/UserDashboard.vue'
import UserService from '@/services/UserService'
import UserStats from '@/components/UserStats.vue'
import UserList from '@/components/UserList.vue'
import UserDetails from '@/components/UserDetails.vue'
// Mock the UserService module
jest.mock('@/services/UserService', () => ({
getUsers: jest.fn()
}))
describe('UserDashboard', () => {
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks()
})
test('displays loading state initially', () => {
// Setup promise that won't resolve during this test
UserService.getUsers.mockReturnValue(new Promise(() => {}))
const wrapper = mount(UserDashboard)
expect(wrapper.text()).toContain('Loading...')
})
test('displays users when API call succeeds', async () => {
// Mock successful API response
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
UserService.getUsers.mockResolvedValue(mockUsers)
const wrapper = mount(UserDashboard, {
stubs: {
// Stub child components for focused testing
UserStats: true,
UserList: true,
UserDetails: true
}
})
// Wait for promises to resolve
await flushPromises()
// Check if child components receive correct props
const userListComponent = wrapper.findComponent(UserList)
expect(userListComponent.props('users')).toEqual(mockUsers)
// No error or loading message should be displayed
expect(wrapper.text()).not.toContain('Loading...')
expect(wrapper.text()).not.toContain('Error:')
})
test('displays error when API call fails', async () => {
// Mock failed API response
UserService.getUsers.mockRejectedValue(new Error('API Error'))
const wrapper = mount(UserDashboard)
// Wait for promises to resolve
await flushPromises()
// Error message should be displayed
expect(wrapper.text()).toContain('Error: API Error')
expect(wrapper.text()).not.toContain('Loading...')
})
test('selects user when selectUser method is called', async () => {
// Mock successful API response
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
UserService.getUsers.mockResolvedValue(mockUsers)
const wrapper = mount(UserDashboard)
await flushPromises()
// Manually call the selectUser method
await wrapper.vm.selectUser(mockUsers[1])
// Check if selectedUser data is updated
expect(wrapper.vm.selectedUser).toEqual(mockUsers[1])
// UserDetails component should receive the selected user as prop
const userDetailsComponent = wrapper.findComponent(UserDetails)
expect(userDetailsComponent.props('user')).toEqual(mockUsers[1])
})
})
Best Practices for Mocking
-
Mock at the right level: Mock at the boundary of your system (e.g., API calls) rather than implementation details.
-
Keep mocks simple: Only mock what's necessary for your test. Over-mocking can make tests brittle and less valuable.
-
Verify interactions: Ensure that your code interacts with mocks in the expected way (e.g., checking if a method was called with specific arguments).
-
Reset mocks between tests: Use
beforeEach
to reset mocks for clean test isolation. -
Avoid mocking the component under test: Mock its dependencies, not the component itself.
-
Match types in mocks: Ensure your mock data structures match the real ones to avoid surprises in production.
Common Mocking Pitfalls
-
Mocking too much: Excessive mocking can result in tests that pass even when the real code would fail.
-
Mocking implementation details: Mocking private methods or internal state can make tests fragile.
-
Incorrect mock setup: Not properly setting up mocks can cause misleading test failures.
-
Missing mock verification: Not verifying that mocks were called as expected.
-
Using real dependencies when mocks would be better: For example, making actual API calls in unit tests.
Summary
Mocking is an essential technique in Vue.js testing that allows you to isolate components, control dependencies, and test various scenarios effectively. In this guide, we've covered:
- Basic mocking of child components and props
- Advanced mocking of API calls, Vuex store, and plugins
- A real-world example of testing a component with multiple dependencies
- Best practices and common pitfalls to avoid
By mastering these mocking techniques, you'll be able to write more robust, reliable, and maintainable tests for your Vue.js applications.
Additional Resources
- Vue Test Utils Documentation
- Jest Mocking Documentation
- Mock Service Worker for API mocking
- Testing Vue.js Applications by Edd Yerburgh
Exercises
-
Basic Mocking: Create a component that displays user information and test it by mocking props and events.
-
API Mocking: Build a component that fetches data from an API and test both success and error scenarios using Jest mocks.
-
Vuex Store Mocking: Test a component that relies on a Vuex store by creating a mock store with required state, getters, and actions.
-
Child Component Integration: Create a parent component that interacts with multiple child components and test it by selectively stubbing children.
-
Advanced Scenario: Build a form component with validation and API submission, then test it by mocking both validation libraries and API calls.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)