Files
gh-alexanderop-claude-skill…/skills/vue-development/references/testing-patterns.md
2025-11-29 17:52:04 +08:00

244 lines
6.7 KiB
Markdown

# Vue Testing Patterns Reference
## Table of Contents
- [Testing Philosophy](#testing-philosophy)
- [Primary Approach: Testing Library](#primary-approach-testing-library)
- [Async Testing](#async-testing)
- [MSW API Mocking](#msw-api-mocking)
- [Testing Library Queries Priority](#testing-library-queries-priority)
- [User Interactions](#user-interactions)
- [Component Library Testing (Fallback)](#component-library-testing-fallback)
- [Common Testing Mistakes](#common-testing-mistakes)
- [Testing Checklist](#testing-checklist)
## Testing Philosophy
**Gold standard:** Test user behavior, not implementation details.
**Note:** For testing composables in isolation, see @testing-composables.md which covers independent vs dependent composables, withSetup helper, and inject testing.
## Primary Approach: Testing Library
```ts
// ✅ CORRECT: Testing Library - user behavior
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John Doe' }
])
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('loads and displays users', async () => {
render(UserList)
// Wait for user-visible content
expect(await screen.findByText('John Doe')).toBeInTheDocument()
})
test('submits form on button click', async () => {
const user = userEvent.setup()
render(UserForm)
await user.type(screen.getByLabelText('Name'), 'Jane')
await user.click(screen.getByRole('button', { name: 'Submit' }))
expect(await screen.findByText('Form submitted')).toBeInTheDocument()
})
// ❌ WRONG: Testing implementation details
import { mount } from '@vue/test-utils'
test('sets isLoading to true', () => {
const wrapper = mount(UserList)
expect(wrapper.vm.isLoading).toBe(true) // Internal state
})
// ❌ WRONG: Arbitrary timeouts
test('loads users', async () => {
const wrapper = mount(UserList)
await new Promise(resolve => setTimeout(resolve, 1000)) // BAD
expect(wrapper.text()).toContain('John')
})
```
**Rules:**
- PRIMARY: `@testing-library/vue` for user-behavior tests
- NEVER use arbitrary `setTimeout()` in tests
- Use `findBy*` queries for async content (built-in waiting)
- Use MSW (`msw`) for API mocking, not test-utils mocks
- Query by accessibility (role, label) not test IDs
- Fallback to `@vue/test-utils` ONLY for component library testing
## Async Testing
```ts
// ✅ CORRECT: findBy* queries (built-in waiting)
expect(await screen.findByText('Loaded')).toBeInTheDocument()
// ✅ CORRECT: waitFor with condition
import { waitFor } from '@testing-library/vue'
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument()
})
// ✅ CORRECT: flushPromises (when using test-utils)
import { flushPromises } from '@vue/test-utils'
await flushPromises()
// ❌ WRONG: Arbitrary timeout
await new Promise(r => setTimeout(r, 1000))
```
## MSW API Mocking
```ts
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
// Define handlers
const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
])
}),
http.post('/api/users', async ({ request }) => {
const newUser = await request.json()
return HttpResponse.json({ id: 3, ...newUser }, { status: 201 })
}),
http.delete('/api/users/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 })
})
]
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// Override handler for specific test
test('handles API error', async () => {
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 })
})
)
render(UserList)
expect(await screen.findByText('Error loading users')).toBeInTheDocument()
})
```
## Testing Library Queries Priority
Use this priority order:
1. **Accessible queries (best)**
- `getByRole('button', { name: 'Submit' })`
- `getByLabelText('Email')`
- `getByPlaceholderText('Enter email')`
- `getByText('Welcome')`
2. **Semantic queries**
- `getByAltText('Profile photo')`
- `getByTitle('Close')`
3. **Test IDs (last resort)**
- `getByTestId('submit-btn')` - only when nothing else works
```ts
// ✅ BEST: Accessible queries
const submitBtn = screen.getByRole('button', { name: 'Submit' })
const emailInput = screen.getByLabelText('Email')
// ⚠️ FALLBACK: Test IDs only if accessibility not possible
const modal = screen.getByTestId('user-modal')
```
## User Interactions
```ts
import userEvent from '@testing-library/user-event'
test('user interactions', async () => {
const user = userEvent.setup()
render(ContactForm)
// Type text
await user.type(screen.getByLabelText('Name'), 'John Doe')
// Click
await user.click(screen.getByRole('button', { name: 'Submit' }))
// Select dropdown
await user.selectOptions(screen.getByLabelText('Country'), 'USA')
// Upload file
const file = new File(['hello'], 'hello.png', { type: 'image/png' })
const input = screen.getByLabelText('Upload avatar')
await user.upload(input, file)
// Keyboard
await user.keyboard('{Enter}')
await user.keyboard('{Escape}')
})
```
## Component Library Testing (Fallback)
When testing component libraries (not applications), @vue/test-utils is acceptable:
```ts
import { mount } from '@vue/test-utils'
test('Button component emits click event', async () => {
const wrapper = mount(Button, {
props: { label: 'Click me' }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
```
**Only use @vue/test-utils when:**
- Testing reusable component libraries
- Need to test component API (props, emits, slots)
- Not testing application behavior
## Common Testing Mistakes
| Mistake | Fix |
|---------|-----|
| `setTimeout(1000)` | Use `findBy*` or `waitFor()` |
| Testing `wrapper.vm.isLoading` | Test visible UI, not internal state |
| `getByTestId` everywhere | Use `getByRole`, `getByLabelText` |
| Mocking with jest.mock | Use MSW for API mocking |
| `wrapper.trigger('click')` | Use `userEvent.click()` for realism |
| Testing implementation | Test user-visible behavior |
## Testing Checklist
- [ ] Import from `@testing-library/vue`, not `@vue/test-utils`
- [ ] Import `userEvent` from `@testing-library/user-event`
- [ ] Use MSW for API mocking
- [ ] Query by `getByRole`, `getByLabelText`, not `getByTestId`
- [ ] Async content? Use `findBy*` queries
- [ ] NEVER use `setTimeout()` for waiting
- [ ] Test what users see, not component internals