Write tests that are maintainable, readable, and actually catch bugs.
The AAA Pattern
Structure tests with Arrange, Act, Assert:
typescriptdescribe('Counter', () => { it('increments count when button clicked', async () => { // Arrange - Set up the test const wrapper = mount(Counter, { props: { initialCount: 5 } }) // Act - Perform the action await wrapper.find('button').trigger('click') // Assert - Verify the result expect(wrapper.find('.count').text()).toBe('6') }) })
Test Behavior, Not Implementation
typescript// ❌ Testing implementation details it('calls increment method', async () => { const wrapper = mount(Counter) const spy = vi.spyOn(wrapper.vm, 'increment') await wrapper.find('button').trigger('click') expect(spy).toHaveBeenCalled() // Brittle! }) // ✅ Testing behavior (what the user sees) it('displays incremented count after click', async () => { const wrapper = mount(Counter, { props: { initialCount: 0 } }) await wrapper.find('button').trigger('click') expect(wrapper.text()).toContain('1') // User-visible behavior })
Test Isolation
typescriptdescribe('UserProfile', () => { let wrapper: VueWrapper // Fresh setup for each test beforeEach(() => { wrapper = mount(UserProfile, { props: { userId: 1 } }) }) afterEach(() => { wrapper.unmount() }) it('displays user name', () => { // Each test starts fresh }) })
Descriptive Test Names
typescript// ❌ Vague names it('works', () => {}) it('handles error', () => {}) // ✅ Descriptive names it('displays error message when API returns 404', () => {}) it('disables submit button while form is submitting', () => {}) it('redirects to dashboard after successful login', () => {})
Testing Edge Cases
typescriptdescribe('SearchInput', () => { it('shows no results message for empty search', async () => { const wrapper = mount(SearchInput) await wrapper.find('input').setValue('') await wrapper.find('button').trigger('click') expect(wrapper.text()).toContain('Enter a search term') }) it('handles special characters in search', async () => { const wrapper = mount(SearchInput) await wrapper.find('input').setValue('<script>alert(1)</script>') // Should sanitize/escape expect(wrapper.find('.search-term').text()).not.toContain('<script>') }) it('trims whitespace from search query', async () => { const wrapper = mount(SearchInput) await wrapper.find('input').setValue(' hello world ') await wrapper.find('button').trigger('click') expect(wrapper.emitted('search')?.[0]).toEqual(['hello world']) }) })
Anti-Patterns to Avoid
1. Testing Props Directly
typescript// ❌ Don't test that Vue works it('receives props', () => { const wrapper = mount(Button, { props: { label: 'Click' } }) expect(wrapper.props('label')).toBe('Click') // Pointless! }) // ✅ Test how props affect the component it('displays the label text', () => { const wrapper = mount(Button, { props: { label: 'Click' } }) expect(wrapper.text()).toBe('Click') })
2. Over-Mocking
typescript// ❌ Mocking everything it('renders', () => { const wrapper = mount(Component, { global: { stubs: ['ChildA', 'ChildB', 'ChildC', 'ChildD'] } }) // Testing a shell with no real behavior }) // ✅ Only mock what's necessary it('displays user data', () => { const wrapper = mount(UserCard, { props: { user: mockUser }, global: { stubs: ['ExpensiveChart'] // Only stub expensive/external } }) })