Vuejs and Vue Test Utils in Practice Part 1
Introduction
Unit testing is a crucial aspect of ensuring code quality, preventing regressions, and promoting discipline within the development team. In this article, we will explore the usage of Vue Test Utils (VTU), the official unit testing utility library for Vue.js applications. Built on top of JEST, VTU provides powerful tools for mounting and interacting with Vue components. Drawing from my personal experience as a Vue.js developer, I will share practical insights and best practices for implementing effective unit tests in your projects. By following these guidelines, you can tackle common challenges and write robust unit tests that meet your specific requirements. Let’s dive into the key principles of unit testing:
- Fast: Unit tests should run quickly since a project can have thousands of unit tests.
- Reliable: unit tests only fail if there is a bug in the underlying code and pass if there is no bug. So try your best to avoid false positives by testing the unit test itself. In other words, the components should be tested as close to the user behavior as possible, in the intended way they are meant to be used.
- independent: must not have any dependencies like other components, nested methods, stores, and API calls.
So please Stay tuned with me to go through different topics here.
In this article, we will cover the following topics:
- Best practices for organizing and structuring your tests
- Writing Unit Tests before code implementation
- Effective practices for unit test descriptions
- Grouping tests
- Testing component properties
- Testing computed properties
- Testing methods
- Testing component slots and scoped slots
- Testing directives
- Testing filters
Best practices for organizing and structuring your tests
- Test File Name: the name of unit test files must be a pattern that reflects the component or feature being tested. for instance: Button.test.js or Button.spec.js
- Directory structure: we should create a directory for our tests, typically name tests or spec.
├── src/
│ ├── components/
│ │ ├── Button.vue
│ │ ├── Input.vue
│ │ └── …
│ ├── services/
│ │ ├── ApiService.js
│ │ └── …
│ └── …
├── tests/
│ ├── unit/
│ │ ├── components/
│ │ │ ├── Button.spec.js
│ │ │ ├── Input.spec.js
│ │ │ └── …
│ │ ├── services/
│ │ │ ├── ApiService.spec.js
│ │ │ └── …
│ │ └── …
│ ├── e2e/
│ │ ├── specs/
│ │ │ ├── home.spec.js
│ │ │ └── …
│ │ └── …
│ └── …
└── …
- Test Suites: Grouping related tests and using describe blocks for them. I’ll explain this one later in a separate topic as “Grouping tests”
- Setup and Teardown: Use appropriate setup and teardown functions to establish a consistent testing environment for each test case. For example, use beforeEach and afterEach hooks to set up and clean up the necessary dependencies or test data.
// myComponent.spec.js
import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
describe('MyComponent', () => {
let wrapper;
beforeEach(() => {
// Mount the component before each test
wrapper = shallowMount(MyComponent);
});
afterEach(() => {
// Destroy the component after each test
wrapper.destroy();
});
it('should render correctly', () => {
// Perform assertions on the mounted component
expect(wrapper.exists()).toBe(true);
expect(wrapper.text()).toContain('Hello, World!');
});
it('should update the message on button click', async () => {
// Simulate a button click
await wrapper.find('button').trigger('click');
// Perform assertions on the updated component state
expect(wrapper.vm.message).toBe('Button clicked!');
});
});
- Helper Functions and Utilities: Encapsulate reusable testing logic or helper functions in separate utility files. This can include functions for creating dummy components, mocking dependencies, or common assertion patterns. Reusing these utilities across multiple tests improves code readability and reduces duplication.
// testUtils.js
export const createDummyComponent = (options) => {
return {
template: '<div>{{ message }}</div>',
data() {
return {
message: 'Hello, World!'
};
},
...options
};
};
// myComponent.spec.js
import { shallowMount } from '@vue/test-utils';
import { createDummyComponent } from '@/testUtils';
import MyComponent from '@/components/MyComponent.vue';
describe('MyComponent', () => {
it('should render correctly', () => {
const DummyComponent = createDummyComponent();
const wrapper = shallowMount(MyComponent, {
components: {
DummyComponent
}
});
expect(wrapper.exists()).toBe(true);
expect(wrapper.text()).toContain('Hello, World!');
});
});
- Test isolation. we should avoid relying on the shared state between tests to ensure predictable and reliable results. Each test case should be able to run independently and produce consistent outcomes.
Writing Unit Tests before code implementation
The ideal approach is to follow a test-driven development (TDD) approach, where you write the unit tests first before implementing the code. This approach is often referred to as “red-green-refactor.”
- Red: Write a failing unit test that describes the desired behavior or functionality.
- Green: Implement the minimal amount of code necessary to make the failing unit test pass.
- Refactor: Improve the code and tests while ensuring that all tests continue to pass.
By writing the tests first, you can define the expected behavior of the code and ensure that it meets the requirements. It also helps in designing cleaner and more maintainable code since you’re focused on writing code that satisfies the tests.
However, in practice, the order in which tests and codes are written can vary depending on the situation. Sometimes you may start with a high-level design or requirements and then write the corresponding tests, while in other cases, you may discover the need for tests as you write the code.
Regardless of the approach you choose, the important thing is to have good coverage of unit tests for your code to ensure its correctness and facilitate future changes and refactoring.
You might have a question in mind how can we start writing unit tests in the midway of a project? To be honest it was my question too so I did a search and provided another article that you can find it here.
Effective practices for unit test descriptions
Discover effective practices for writing high-quality unit test descriptions. Learn from the experience of refining my own descriptions after facing rejections. Find out the fundamental principles and guidelines that helped me improve the clarity and effectiveness of my test descriptions.
- Be more specific
Instead of using general terms like “the component” or “the input field,” try to provide more specific and descriptive details. Specify which component or input field you are referring to. This helps to avoid ambiguity and makes the test case description more clear. - Use imperative language
Instead of using the word “should” in your test case descriptions, use imperative language to describe the expected behavior. For example, instead of “It should render the component with the correct props,” you can write “Renders the component with the correct props.” This makes the description more direct and action-oriented. - Focus on the behavior
Rather than describing implementation details, focus on describing the desired behavior or outcome of the test. Describe what the user should experience or observe when interacting with the component. - Keep test descriptions short, concise, and free of repetition
For instance, use ‘handles empty input’ instead of ‘handles situations where the user has not entered any data into the input field’.”
describe('MyComponent', () => {
it('renders with the correct props', () => {
// Test logic goes here
});
it('handles empty input correctly', () => {
// Test logic goes here
});
it('displays an error message on invalid input', () => {
// Test logic goes here
});
});
Grouping tests
Grouping tests is a recommended practice to organize your test suite and enhance its manageability. By grouping related unit tests together, you can improve the readability and maintainability of your tests. Additionally, grouping tests allows for better control over test execution, as you can selectively run specific subsets of tests during development or as part of your CI/CD pipeline.
One of the advantages of grouping tests is the ability to create and destroy a wrapper for a group of unit tests efficiently. This can significantly improve test performance by reducing unnecessary setup and teardown operations for each individual test case. By encapsulating related tests within a single wrapper, you can achieve better test execution efficiency and optimize the overall testing process.
By adopting a systematic approach to grouping tests, you can ensure that your test suite remains well-organized, scalable, and easy to maintain. Let’s explore some examples and best practices for effectively grouping unit tests in your Vue.js projects.
describe('User Management', () => {
let userService;
beforeEach(() => {
// Initialize the userService before each test in the group
userService = new UserService();
});
afterEach(() => {
// Clean up any resources after each test in the group
userService.reset();
});
describe('User Registration', () => {
it('should register a new user', () => {
// Test logic for registering a new user
});
it('should not allow duplicate usernames', () => {
// Test logic for handling duplicate usernames during registration
});
});
describe('User Authentication', () => {
it('should authenticate a valid user', () => {
// Test logic for authenticating a valid user
});
it('should reject an invalid user', () => {
// Test logic for rejecting an invalid user during authentication
});
});
});
Testing component properties
Testing component props involves verifying that the component behaves correctly when given different props.
In Practice, the wrapper object provides a props
option that can be used to pass in props to the component being tested. This allows you to control the values of the props and then test different scenarios.
Here’s an example of testing a component prop in a Vue component:
import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
describe('MyComponent', () => {
it('should render the prop value', () => {
const wrapper = shallowMount(MyComponent, {
props: {
message: 'Hello World'// Pass in props
}
});
// Verify that the component renders the correct content
expect(wrapper.find('p').text()).toEqual('Hello World');
});
it('should emit an event when the button is clicked', () => {
const wrapper = shallowMount(MyComponent, {
props: {
message: 'Hello World'
}
});
// Simulate a click event on the button
wrapper.find('button').trigger('click');
// Verify that the component emitted the correct event
expect(wrapper.emitted('button-clicked')).toBeTruthy();
});
});
Keep in mind to Test edge cases and error handling. For instance, you might test what happens when a required prop is missing, or when an invalid prop value is passed in.
// Test an edge case
it('renders default value when prop is not passed', () => {
const wrapper = mount(MyComponent);
expect(wrapper.text()).toContain('Default Value');
});
// Test an edge case
it('renders error message when prop is invalid', () => {
const wrapper = mount(MyComponent, {
propsData: {
myProp: null // or any other invalid value
}
});
expect(wrapper.text()).toContain('Invalid Prop');
});
Testing whether a component requires certain props to be passed is not always necessary. It depends on the specific requirements and complexity of your component. For example, If the prop is optional and the component can function properly even without it, you may choose not to write a test specifically for the presence of that prop. However, if the prop is required for the component to work correctly and its absence would result in unexpected behavior or errors, it can be beneficial to have a test that ensures the component throws an error or handles the missing prop appropriately.
it('requires prop "message" to be passed', () => {
expect(() => shallowMount(MyComponent)).toThrow('Missing required prop: "message"');
});
Testing computed properties
To test a computed property, we need the wrapper object's vm
property to access the component's Vue instance, and then test the computed property like a regular function by passing in mock data as arguments and verifying the expected result.
Here's an example:
import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
describe('MyComponent', () => {
it('should calculate the correct sum of two values', () => {
const wrapper = shallowMount(MyComponent, {
propsData: {
value1: 2,
value2: 3,
},
});
expect(wrapper.vm.sum).toEqual(5);
});
});
Just like Alwyse, you also need to consider edge cases specific to your functionality. In the following code block, I’ll provide an example that may not directly apply to your specific case, but it’s intended to serve as inspiration. Feel free to adapt and modify it as needed for your purposes.
// Test an edge case
it('handles edge case when input is zero', () => {
const wrapper = mount(MyComponent, {
data() {
return {
inputValue: 0
};
}
});
expect(wrapper.vm.computedProperty).toBe(0);
});
// Test an edge case
it('handles edge case when input is negative', () => {
const wrapper = mount(MyComponent, {
data() {
return {
inputValue: -5
};
}
});
expect(wrapper.vm.computedProperty).toBe('Invalid');
});
Testing methods
To test a method, we can simulate the user action or event that triggers the method, and then assert that the method performs the expected behavior or state update.
Here’s an example:
import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
describe('MyComponent', () => {
it('should increment the counter when the button is clicked', () => {
const wrapper = shallowMount(MyComponent);
const button = wrapper.find('button');
button.trigger('click');
expect(wrapper.vm.counter).toEqual(1);
});
});
Testing edge cases and error-handling methods can be complex, requiring a dedicated article to delve into the details. However, I’d like to highlight some commons points related to edge cases and complex scenarios:
- Edge cases for input parameters.
- Handling null or undefined values
- Error handling
- Handling empty arrays or objects
- Complex data structures
- Performance testing
Remember to keep your test cases focused on the behavior and functionality of the method, covering both typical scenarios and exceptional cases.
Just a general example that might not be required in your case and it’s intended to serve as inspiration
// Example component with a method to test
const MyComponent = {
methods: {
calculateSquareRoot(number) {
if (number < 0) {
throw new Error('Invalid input: Number must be non-negative.');
}
return Math.sqrt(number);
},
},
};
// Test suite for the method
describe('MyComponent', () => {
describe('calculateSquareRoot', () => {
it('returns the square root of a positive number', () => {
const wrapper = shallowMount(MyComponent);
const result = wrapper.vm.calculateSquareRoot(25);
expect(result).toBe(5);
});
it('throws an error for a negative number', () => {
const wrapper = shallowMount(MyComponent);
expect(() => {
wrapper.vm.calculateSquareRoot(-10);
}).toThrow('Invalid input: Number must be non-negative.');
});
it('returns NaN for an undefined input', () => {
const wrapper = shallowMount(MyComponent);
const result = wrapper.vm.calculateSquareRoot(undefined);
expect(result).toBeNaN();
});
it('returns 0 for the square root of 0', () => {
const wrapper = shallowMount(MyComponent);
const result = wrapper.vm.calculateSquareRoot(0);
expect(result).toBe(0);
});
it('handles large numbers correctly', () => {
const wrapper = shallowMount(MyComponent);
const result = wrapper.vm.calculateSquareRoot(1e20);
expect(result).toBe(1e10);
});
});
});
Testing component slots and scoped slots
We need to check that the component renders correctly when it receives different content through its slots. The wrapper object provides a slots
option that can be used to pass in content for the component's slots. Verify the component’s rendering for each slot content.
import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
describe('MyComponent', () => {
it('should render slot content', () => {
const wrapper = shallowMount(MyComponent, {
slots: {
default: '<p>Slot content</p>'
}
});
// Verify that the component renders the slot content in the correct location
expect(wrapper.find('p').text()).toEqual('Slot content');
});
it('should emit an event when the slot content is clicked', () => {
const wrapper = shallowMount(MyComponent, {
slots: {
default: '<button>Click me</button>'
}
});
// Simulate a click event on the slot content
wrapper.find('button').trigger('click');
// Verify that the component emitted the correct event
expect(wrapper.emitted('slot-clicked')).toBeTruthy();
});
// in the situation of having a scoped slot we try to test it like this.
it('should render scoped slot content', () => {
const wrapper = shallowMount(MyComponent, {
scopedSlots: {
header: '<p slot-scope="props">Header slot content: {{ props.title }}</p>'
},
propsData: {
title: 'My Component'
}
});
// Verify that the scoped slot content is rendered
expect(wrapper.find('.header-slot').text()).toEqual('Header slot content: My Component');
});
});
As with testing props and methods, it’s important to test edge cases and error handling for slots. For example, you might test what happens when a required slot is missing, or when an invalid slot content is passed in.
it('should display a message when the slot is missing', () => {
const wrapper = shallowMount(MyComponent);
// Verify that the component displays the error message when the slot is missing
expect(wrapper.find('.error-message').text()).toEqual('Slot content is missing');
});
it('should display a message when an invalid slot is passed in', () => {
const wrapper = shallowMount(MyComponent, {
slots: {
default: '<span>Invalid slot content</span>'
}
});
// Verify that the component displays the error message when an invalid slot is passed in
expect(wrapper.find('.error-message').text()).toEqual('Invalid slot content');
});
The same as me you might wonder how we can test scoped slot.
Scoped slots, allow the parent component to pass not just content but also data and methods to the child component. The child component can receive this data and use it within the slot template.
When it comes to testing components with scoped slots, it’s generally recommended to focus on testing the parent component that defines the scoped slot. The reason is that the parent component is responsible for passing data, props, and any necessary methods to the child component via the scoped slot. The child component, on the other hand, mainly renders the content received from the parent. In the implementation, for accessing the scoped slot using the appropriate syntax based on the version of Vue.js you are using:
- For Vue 2.x:
wrapper.vm.$scopedSlots.slotName()
- For Vue 3.x:
wrapper.vm.$slots.slotName({})
ReplaceslotName
with the actual name of the scoped slot.
And also make assertions on the rendered content within the scoped slot, including any interactions or data manipulations performed within the slot.
import { shallowMount } from '@vue/test-utils';
import MyParentComponent from '@/components/MyParentComponent.vue';
import MyChildComponent from '@/components/MyChildComponent.vue';
describe('MyParentComponent', () => {
it('renders the content within the scoped slot correctly', () => {
const wrapper = shallowMount(MyParentComponent, {
slots: {
default: '<div>Slot content</div>', // Content to be rendered within the scoped slot
},
// Other necessary props and data
});
const childComponent = wrapper.findComponent(MyChildComponent);
// Access the scoped slot and make assertions
const scopedSlotContent = childComponent.vm.$scopedSlots.default();
expect(scopedSlotContent.text()).toBe('Slot content');
});
});
Testing component events and methods that emit events
Verify that the component correctly emits events when certain actions occur, such as a button click or a form submission. And here’s how you can test it using Vue Test Utils.
Imagine we have such a component:
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
methods: {
handleClick() {
this.$emit('button-clicked')
}
}
}
</script>
Its unit test can be like this:
import { mount } from '@vue/test-utils'
import Button from '@/components/Button.vue'
describe('Button', () => {
it('emits a "button-clicked" event when clicked', () => {
const wrapper = mount(Button)
wrapper.find('button').trigger('click')
expect(wrapper.emitted('button-clicked')).toBeTruthy()
})
})
Testing Vue directives
Directives are custom functions or hooks that can be used to manipulate the DOM or apply special behaviors to elements. In the unit test, we need to mount a Vue component that uses the directive and performs actions to trigger the directive (I suggest creating a dummy function and using that one), and then assert the expected changes or effects caused by the directive.
// myDirective.js
export default {
bind(el, binding) {
// Do something when the directive is bound to an element
},
update(el, binding) {
// Do something when the element's bound value is updated
},
unbind(el, binding) {
// Do something when the directive is unbound from an element
},
};
// DummyComponent.vue
<template>
<div v-my-directive>
<!-- Dummy component content -->
</div>
</template>
// myDirective.spec.js
import { mount } from '@vue/test-utils';
import myDirective from './myDirective.js';
const createDummyComponent = () => ({
template: '<div v-my-directive></div>',
directives: {
'my-directive': myDirective,
},
});
describe('myDirective', () => {
it('binds the directive to the element in a dummy component', () => {
const wrapper = mount(createDummyComponent(), {
directives: {
'my-directive': myDirective,
},
});
// Assert the expected changes or effects on the element
});
it('updates the element when the bound value changes in a dummy component', () => {
const wrapper = mount(createDummyComponent(), {
directives: {
'my-directive': myDirective,
},
});
// Trigger an update that affects the directive
wrapper.vm.someValue = 'new value';
// Assert the expected changes or effects on the element
});
it('unbinds the directive from the element in a dummy component', () => {
const wrapper = mount(createDummyComponent(), {
directives: {
'my-directive': myDirective,
},
});
// Unmount the component to trigger unbinding
wrapper.unmount();
// Assert the expected changes or effects on the element
});
});
Testing Vue Filters
Testing Vue Filters: Filters are custom functions used for transforming data in Vue templates. To test a Vue filter, you can follow these steps: for the test cases, write test cases to verify the behavior of the filter, call the filter function with different input values, and assert the expected output values.
// myFilter.js
export default function myFilter(value) {
// Apply some transformation on the input value
return value.toUpperCase();
}
// myFilter.spec.js
import myFilter from './myFilter.js';
describe('myFilter', () => {
it('transforms the input value', () => {
const input = 'hello world';
const output = myFilter(input);
expect(output).toBe('HELLO WORLD');
});
it('handles edge cases', () => {
const input = null;
const output = myFilter(input);
expect(output).toBe('');
});
});
To ensure brevity and provide access to the full article, please click on “Vuejs and Vue Test Utils in Practice Part 2” to continue reading. In the upcoming article, I will delve into further discussions on best practices in unit testing, covering a range of insightful topics.
- Testing Vue mixins
- Testing the usage of Vue mixins
- Testing Vue plugins
- Testing Usage of Plugins
- Testing API calls
- Testing form inputs and validation
- Using snapshot
- Testing the lifecycle hooks
- Debugging Vue unit tests