Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 testing best practices with Vitest, Vue Test Utils, component testing, mocking, and Playwright E2E.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/testing-component-blackbox-approach.md
1---2title: Test Components Using Blackbox Approach - Focus on Behavior Not Implementation3impact: HIGH4impactDescription: Implementation-aware tests become brittle and break during refactoring, leading to high maintenance burden5type: best-practice6tags: [vue3, testing, component-testing, vitest, vue-test-utils, blackbox]7---89# Test Components Using Blackbox Approach - Focus on Behavior Not Implementation1011**Impact: HIGH** - Tests that rely on implementation details (internal state, private methods, component structure) break during refactoring even when functionality remains correct. This leads to false negatives and high test maintenance burden.1213Follow Kent C. Dodds' testing philosophy: "The more your tests resemble how your software is used, the more confidence they can give you."1415## Task Checklist1617- [ ] Test what the component does, not how it does it18- [ ] Query elements by user-visible attributes (text, role, testid)19- [ ] Simulate user interactions (click, type) rather than calling methods directly20- [ ] Assert on rendered output, emitted events, and visible state changes21- [ ] Avoid accessing component internal state or private methods22- [ ] Use data-testid attributes for elements without semantic meaning2324**Incorrect:**25```javascript26import { mount } from '@vue/test-utils'27import Counter from './Counter.vue'2829// BAD: Testing implementation details30test('counter increments', async () => {31const wrapper = mount(Counter)3233// Accessing internal state directly34expect(wrapper.vm.count).toBe(0)3536// Calling internal method instead of simulating user action37wrapper.vm.increment()3839// Checking internal state instead of visible output40expect(wrapper.vm.count).toBe(1)41})4243// BAD: Testing component structure44test('has increment button', () => {45const wrapper = mount(Counter)4647// Testing implementation detail - what if button becomes an anchor?48expect(wrapper.find('button').exists()).toBe(true)49})50```5152**Correct:**53```javascript54import { mount } from '@vue/test-utils'55import Counter from './Counter.vue'5657// CORRECT: Testing behavior like a user would58test('counter displays updated value after clicking increment', async () => {59const wrapper = mount(Counter, {60props: { max: 10 }61})6263// Assert initial visible state64expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('0')6566// Simulate user action67await wrapper.find('[data-testid="increment-button"]').trigger('click')6869// Assert visible result70expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('1')71})7273// CORRECT: Testing emitted events (public API)74test('emits change event with new value when incremented', async () => {75const wrapper = mount(Counter)7677await wrapper.find('[data-testid="increment-button"]').trigger('click')7879expect(wrapper.emitted('change')).toHaveLength(1)80expect(wrapper.emitted('change')[0]).toEqual([1])81})82```8384## Using @testing-library/vue for Better Blackbox Tests8586```javascript87import { render, screen, fireEvent } from '@testing-library/vue'88import Counter from './Counter.vue'8990// Testing Library encourages accessible, user-centric queries91test('increments counter on button click', async () => {92render(Counter)9394// Query by role - how screen readers see it95const button = screen.getByRole('button', { name: /increment/i })96const display = screen.getByText('0')9798await fireEvent.click(button)99100expect(screen.getByText('1')).toBeInTheDocument()101})102```103104## What to Test vs What Not to Test105106### DO Test (Public Interface)107```javascript108// Props affect rendered output109test('shows title from props', () => {110const wrapper = mount(Card, {111props: { title: 'Hello World' }112})113expect(wrapper.text()).toContain('Hello World')114})115116// Slots render correctly117test('renders slot content', () => {118const wrapper = mount(Card, {119slots: { default: '<p>Slot content</p>' }120})121expect(wrapper.text()).toContain('Slot content')122})123124// Emitted events125test('emits close event when X clicked', async () => {126const wrapper = mount(Modal)127await wrapper.find('[data-testid="close-button"]').trigger('click')128expect(wrapper.emitted('close')).toBeTruthy()129})130```131132### DON'T Test (Implementation Details)133```javascript134// Don't test internal computed properties135// Don't test internal methods136// Don't test component options/setup internals137// Don't test that specific child components are rendered (unless critical)138// Don't rely exclusively on snapshot tests for correctness139```140141## Reference142- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)143- [Vue Test Utils - Testing Philosophy](https://test-utils.vuejs.org/guide/)144- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles)145