
Custom Events and Component Communication in Vue - `emit`, `provide/inject`, and When to Use a Store
} -->
Testing frontend components is crucial for building reliable and maintainable applications. Vitest is a fast, lightweight test runner designed for modern frontend projects, especially those using Vue or React. In this post, I’ll walk you through how to write effective component tests in Vitest, including examples of unit and integration tests, and tips to avoid common pitfalls.
Testing helps you catch bugs early, document expected behavior, and refactor code confidently. Components are the building blocks of your UI, so making sure each piece works as expected is a great investment.
npm install -D vitest @testing-library/vue
# or for React
npm install -D vitest @testing-library/react
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
},
})
Unit tests focus on a single component in isolation, often mocking dependencies.
<!-- Button.vue -->
<template>
<button @click="onClick" :class="variantClass">
<slot />
</button>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, computed } from 'vue'
const props = defineProps({
variant: {
type: String,
default: 'primary',
},
})
const emit = defineEmits(['click'])
const variantClass = computed(() => {
return props.variant === 'primary' ? 'btn-primary' : 'btn-secondary'
})
function onClick(event: MouseEvent) {
emit('click', event)
}
</script>
<style>
.btn-primary {
background-color: blue;
color: white;
}
.btn-secondary {
background-color: gray;
color: white;
}
</style>
import { render, fireEvent } from '@testing-library/vue'
import Button from './Button.vue'
import { describe, it, expect } from 'vitest'
describe('Button', () => {
it('renders with primary class by default', () => {
const { getByRole } = render(Button, { slots: { default: 'Click me' } })
const button = getByRole('button')
expect(button.className).toContain('btn-primary')
})
it('emits click event when clicked', async () => {
const { getByRole, emitted } = render(Button, { slots: { default: 'Click me' } })
const button = getByRole('button')
await fireEvent.click(button)
expect(emitted()).toHaveProperty('click')
})
it('renders secondary variant class', () => {
const { getByRole } = render(Button, { props: { variant: 'secondary' }, slots: { default: 'Click me' } })
const button = getByRole('button')
expect(button.className).toContain('btn-secondary')
})
})
Integration tests verify how multiple components work together or how a component behaves with real data.
<!-- LoginForm.vue -->
<template>
<form @submit.prevent="submitForm">
<input v-model="username" placeholder="Username" />
<Button @click="submitForm">Login</Button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Button from './Button.vue'
const username = ref('')
function submitForm() {
if (username.value) {
alert(`Logging in ${username.value}`)
}
}
</script>
import { render, fireEvent } from '@testing-library/vue'
import LoginForm from './LoginForm.vue'
import { describe, it, expect, vi } from 'vitest'
describe('LoginForm', () => {
it('submits the form with username', async () => {
const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {})
const { getByPlaceholderText, getByText } = render(LoginForm)
const input = getByPlaceholderText('Username')
await fireEvent.update(input, 'bartek')
const button = getByText('Login')
await fireEvent.click(button)
expect(alertMock).toHaveBeenCalledWith('Logging in bartek')
alertMock.mockRestore()
})
})
Don’t test implementation details: focus on what the user sees and interacts with, not internal component state.
Avoid flaky tests: make sure async updates and events are awaited properly (await fireEvent.click(…)).
Mock external dependencies: if your component calls APIs or uses global stores, mock them to keep tests fast and isolated.
Write small, focused tests: each test should verify one behavior or scenario.
Vitest combined with Testing Library offers a great developer experience for writing reliable, readable tests. Start with small unit tests for atomic components, then add integration and e2e tests for full user flows.
If you’re just starting, try writing a test for one component today — you’ll quickly see the benefits!
Bartłomiej Nowak
Programmer
Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.