Initial commit
This commit is contained in:
243
skills/vue-development/references/testing-patterns.md
Normal file
243
skills/vue-development/references/testing-patterns.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user