Automatic Component Testing with Vitest and Testing Library
Testing is an essential part of software development. It ensures our components work as expected and helps us catch bugs early in the development process. In this article, we'll explore how to automatically test Vue.js components using Vitest and Testing Library.
When working on a Vite project, Vitest is the best choice to test our components. It's a test runner optimized for Vite and is very easy to use. Testing Library builds on Vue Test Utils and provides a set of utilities to test our components in a user-centric way.
For example, Testing Library encourages us to test the behavior of our components instead of their implementation. This is a powerful concept that allows us to test components in a way that is closer to the user experience and makes refactoring easier.
Setting Up Vitest
To get started, we need to install Vitest and Testing Library in our packages/huchet-vue
package:
pnpm add -D vitest @testing-library/vue jsdom
Since we are testing frontend components, we need a DOM environment. To avoid launching a real browser, which is slow and resource-consuming, we can use JSDOM, a lightweight implementation of the DOM in Node.js.
To make Vitest work with JSDOM, we need to configure it in the vite.config.ts
file:
/// <reference types="vitest" />
import { defineConfig } from 'vite'
export default defineConfig({
// ...
test: {
globals: true,
environment: 'jsdom',
},
})
Note
/// <reference types="vitest" />
is crucial to ensure the correct types are available in the Vite configuration file. It augments the Vite configuration file with the Vitest types.
In the package.json
file of packages/huchet-vue
, add two new scripts to run the tests:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
The first script runs the tests once, and the second script runs the tests in watch mode if it does not detect a CI environment.
TypeScript Configuration
Our test files form a new TypeScript context. We need to access the Node.js API but not the browser environment. As with any new TypeScript context, we configure it through a new TSConfig file.
Create a new tsconfig.test.json
file that extends from tsconfig.node.json
and includes the test.ts
files:
{
"extends": "./tsconfig.node.json",
"include": ["src/**/*.test.ts"]
}
In the tsconfig.app.json
, we exclude the test.ts
files, similar to how we excluded the story.vue
files:
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"src/**/*.ts",
"src/**/*.vue"
],
"exclude": [
"src/**/*.story.vue",
"src/**/*.test.ts"
]
}
In our main TSConfig file, we add a reference to the tsconfig.test.json
file:
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.story.json"
},
{
"path": "./tsconfig.test.json"
}
]
}
Writing Our First Test
Every component is isolated and encapsulated in its folder from the start. Testing will follow the same structure. Create a src/Button/Button.test.ts
file and write our first test:
touch src/Button/Button.test.ts
import { fireEvent, render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
import Button from './Button.vue'
describe('button', () => {
it('should emit a click event when clicked', async () => {
const { emitted } = render(Button, {
props: {
label: 'Hello',
},
})
await fireEvent.click(screen.getByText('Hello'))
expect(emitted()).toMatchInlineSnapshot(`
{
"click": [
[
MouseEvent {
"isTrusted": false,
},
],
],
}
`)
})
describe('solid', () => {
it('should render a solid button', () => {
const button = render(Button, {
props: {
label: 'Hello',
variant: 'solid',
},
})
expect(button.html()).toMatchSnapshot()
})
})
describe('outline', () => {
it('should render an outline button', () => {
const button = render(Button, {
props: {
label: 'Hello',
variant: 'outline',
},
})
expect(button.html()).toMatchSnapshot()
})
})
})
In the first test, we use the render
function from Testing Library to render our Button
component. We also use the fireEvent
function to simulate a click event on the button. Finally, we utilize the expect
function from Vitest to assert that the component emits a click
event when clicked.
In the following tests, we verify the solid
and outline
variants of the button. We use the toMatchSnapshot
function from Vitest to assert that the component renders correctly.
These tests reflect two key approaches I prefer when testing components:
- Testing the behavior of the component. For instance, does it emit the correct events when clicked?
- Testing the appearance of the component. For example, does it render correctly with different variants?
Most of the time, these two types of tests are sufficient to ensure our components work as expected, and they can be easily achieved using snapshot testing.
Tip
Snapshot testing is a powerful tool that allows us to capture the output of our components and compare it against a reference snapshot. This is very useful for checking the appearance of our components, ensuring they render correctly, and avoiding manually writing the expected HTML.
We can run our tests with the following command:
pnpm run test
Everything works as expected!
DEV v2.1.5 /huchet-vue/packages/huchet-vue
✓ src/Button/Button.test.ts (3)
✓ button (3)
✓ should emit a click event when clicked
✓ solid (1)
✓ should render a solid button
✓ outline (1)
✓ should render an outline button
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 00:00:00
Duration 536ms (transform 42ms, setup 0ms, collect 142ms, tests 22ms, environment 237ms, prepare 34ms)
Typechecking Tests
Besides tests, we can ensure every type in the project is correct. This is a straightforward way to detect errors before they happen, but it doesn't replace the tests. We can add a new script to the package.json
file to run the typecheck:
{
"scripts": {
"typecheck": "vue-tsc -p tsconfig.app.json --noEmit"
}
}
This script uses the vue-tsc
command to typecheck the project within the context of the tsconfig.app.json
file. The --noEmit
option prevents transpiling the TypeScript files into JavaScript files, focusing solely on typechecking without building.
Thanks for reading! My name is Estéban, and I love to write about web development.
I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.
If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.
I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!