How to Write Effective Component Tests with Vitest


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.


Why Test Components?

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.


Getting Started with Vitest

  1. Install Vitest (and Vue Testing Library or React Testing Library if needed):
npm install -D vitest @testing-library/vue
# or for React
npm install -D vitest @testing-library/react
  1. Configure Vitest in your vite.config.ts or vite.config.js:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
  },
})

Writing Unit Tests for Atomic Components

Unit tests focus on a single component in isolation, often mocking dependencies.

Example: Button Component

<!-- 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>

Test: Button.spec.ts

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')
  })
})

Writing Integration Tests

Integration tests verify how multiple components work together or how a component behaves with real data.

Example: Form with Input and Button

<!-- 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>

Test: LoginForm.spec.ts

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()
  })
})

Common Pitfalls and Tips

  • 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.

Final Thoughts

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

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.

Recent Posts