测试策略 #
概述 #
测试是保证应用质量的重要环节。本章将介绍 Alpine.js 应用的测试方法和最佳实践。
测试类型 #
| 类型 | 范围 | 速度 | 工具 |
|---|---|---|---|
| 单元测试 | 函数/组件 | 快 | Vitest/Jest |
| 集成测试 | 组件交互 | 中 | Vitest + Testing Library |
| E2E 测试 | 完整流程 | 慢 | Playwright/Cypress |
单元测试 #
配置 Vitest #
javascript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts']
}
})
测试设置 #
typescript
import { JSDOM } from 'jsdom'
import Alpine from 'alpinejs'
const dom = new JSDOM('<!DOCTYPE html>')
global.document = dom.window.document
global.window = dom.window as any
global.Alpine = Alpine
window.Alpine = Alpine
测试组件逻辑 #
typescript
import { describe, it, expect, beforeEach } from 'vitest'
import Alpine from 'alpinejs'
describe('Counter Component', () => {
let component: ReturnType<typeof counter>
function counter(initial = 0) {
return {
count: initial,
increment() { this.count++ },
decrement() { this.count-- },
reset() { this.count = initial }
}
}
beforeEach(() => {
component = counter(0)
})
it('should initialize with correct value', () => {
expect(component.count).toBe(0)
})
it('should increment count', () => {
component.increment()
expect(component.count).toBe(1)
})
it('should decrement count', () => {
component.increment()
component.decrement()
expect(component.count).toBe(0)
})
it('should reset to initial value', () => {
component.increment()
component.increment()
component.reset()
expect(component.count).toBe(0)
})
})
测试计算属性 #
typescript
describe('Todo List', () => {
function todoList() {
return {
todos: [
{ id: 1, text: 'Task 1', completed: false },
{ id: 2, text: 'Task 2', completed: true }
],
get activeTodos() {
return this.todos.filter(t => !t.completed)
},
get completedTodos() {
return this.todos.filter(t => t.completed)
},
get remainingCount() {
return this.activeTodos.length
}
}
}
it('should filter active todos', () => {
const component = todoList()
expect(component.activeTodos.length).toBe(1)
})
it('should filter completed todos', () => {
const component = todoList()
expect(component.completedTodos.length).toBe(1)
})
it('should count remaining todos', () => {
const component = todoList()
expect(component.remainingCount).toBe(1)
})
})
测试异步操作 #
typescript
describe('Async Component', () => {
function asyncComponent() {
return {
data: null as any,
loading: false,
error: null as string | null,
async fetchData() {
this.loading = true
try {
const res = await fetch('/api/data')
this.data = await res.json()
} catch (e) {
this.error = (e as Error).message
} finally {
this.loading = false
}
}
}
}
it('should handle successful fetch', async () => {
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ name: 'Test' })
})
const component = asyncComponent()
await component.fetchData()
expect(component.loading).toBe(false)
expect(component.data).toEqual({ name: 'Test' })
expect(component.error).toBeNull()
})
it('should handle fetch error', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
const component = asyncComponent()
await component.fetchData()
expect(component.loading).toBe(false)
expect(component.error).toBe('Network error')
})
})
集成测试 #
使用 Testing Library #
typescript
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, fireEvent, screen } from '@testing-library/dom'
import userEvent from '@testing-library/user-event'
import Alpine from 'alpinejs'
describe('Dropdown Integration', () => {
beforeEach(() => {
document.body.innerHTML = ''
})
function renderDropdown() {
const html = `
<div x-data="{ open: false }">
<button @click="open = !open" data-testid="toggle">
Toggle
</button>
<div x-show="open" data-testid="menu">
Menu Content
</div>
</div>
`
document.body.innerHTML = html
Alpine.initTree(document.body)
return {
toggle: screen.getByTestId('toggle'),
menu: screen.getByTestId('menu')
}
}
it('should toggle menu visibility', async () => {
const { toggle, menu } = renderDropdown()
expect(menu).not.toBeVisible()
await fireEvent.click(toggle)
expect(menu).toBeVisible()
await fireEvent.click(toggle)
expect(menu).not.toBeVisible()
})
})
测试表单 #
typescript
describe('Form Integration', () => {
function renderForm() {
const html = `
<form x-data="{
email: '',
password: '',
errors: {},
submit() {
this.errors = {}
if (!this.email) this.errors.email = 'Required'
if (!this.password) this.errors.password = 'Required'
}
}" @submit.prevent="submit()">
<input x-model="email" data-testid="email">
<span x-show="errors.email" data-testid="email-error" x-text="errors.email"></span>
<input x-model="password" data-testid="password">
<span x-show="errors.password" data-testid="password-error" x-text="errors.password"></span>
<button type="submit" data-testid="submit">Submit</button>
</form>
`
document.body.innerHTML = html
Alpine.initTree(document.body)
return {
email: screen.getByTestId('email') as HTMLInputElement,
password: screen.getByTestId('password') as HTMLInputElement,
submit: screen.getByTestId('submit'),
emailError: screen.getByTestId('email-error'),
passwordError: screen.getByTestId('password-error')
}
}
it('should show validation errors', async () => {
const { submit, emailError, passwordError } = renderForm()
await fireEvent.click(submit)
expect(emailError).toBeVisible()
expect(passwordError).toBeVisible()
})
it('should not show errors when filled', async () => {
const { email, password, submit, emailError, passwordError } = renderForm()
await userEvent.type(email, 'test@example.com')
await userEvent.type(password, 'password123')
await fireEvent.click(submit)
expect(emailError).not.toBeVisible()
expect(passwordError).not.toBeVisible()
})
})
E2E 测试 #
Playwright 配置 #
typescript
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: true
}
})
E2E 测试示例 #
typescript
import { test, expect } from '@playwright/test'
test.describe('Todo App', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('should add a new todo', async ({ page }) => {
await page.fill('[data-testid="new-todo"]', 'Buy groceries')
await page.click('[data-testid="add-todo"]')
await expect(page.locator('[data-testid="todo-item"]')).toContainText('Buy groceries')
})
test('should complete a todo', async ({ page }) => {
await page.fill('[data-testid="new-todo"]', 'Task to complete')
await page.click('[data-testid="add-todo"]')
await page.click('[data-testid="todo-checkbox"]')
await expect(page.locator('[data-testid="todo-item"]')).toHaveClass(/completed/)
})
test('should delete a todo', async ({ page }) => {
await page.fill('[data-testid="new-todo"]', 'Task to delete')
await page.click('[data-testid="add-todo"]')
await page.click('[data-testid="delete-todo"]')
await expect(page.locator('[data-testid="todo-item"]')).not.toBeVisible()
})
})
测试用户认证 #
typescript
test.describe('Authentication', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="email"]', 'user@example.com')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="login-btn"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('[data-testid="user-name"]')).toBeVisible()
})
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="email"]', 'wrong@example.com')
await page.fill('[data-testid="password"]', 'wrongpassword')
await page.click('[data-testid="login-btn"]')
await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
})
})
测试 Store #
typescript
describe('Cart Store', () => {
let cartStore: CartStore
beforeEach(() => {
cartStore = {
items: [],
add(product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
},
remove(productId) {
this.items = this.items.filter(i => i.id !== productId)
},
get total() {
return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
},
get count() {
return this.items.reduce((sum, i) => sum + i.quantity, 0)
}
}
})
it('should add item to cart', () => {
cartStore.add({ id: 1, name: 'Product', price: 100 })
expect(cartStore.items.length).toBe(1)
expect(cartStore.count).toBe(1)
})
it('should increment quantity for existing item', () => {
cartStore.add({ id: 1, name: 'Product', price: 100 })
cartStore.add({ id: 1, name: 'Product', price: 100 })
expect(cartStore.items.length).toBe(1)
expect(cartStore.count).toBe(2)
})
it('should calculate total correctly', () => {
cartStore.add({ id: 1, name: 'Product A', price: 100 })
cartStore.add({ id: 2, name: 'Product B', price: 50 })
expect(cartStore.total).toBe(150)
})
it('should remove item from cart', () => {
cartStore.add({ id: 1, name: 'Product', price: 100 })
cartStore.remove(1)
expect(cartStore.items.length).toBe(0)
})
})
Mock 策略 #
Mock Fetch #
typescript
const mockFetch = vi.fn()
global.fetch = mockFetch
beforeEach(() => {
mockFetch.mockReset()
})
it('should fetch data', async () => {
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ data: 'test' })
})
const result = await fetchData()
expect(result).toEqual({ data: 'test' })
})
Mock LocalStorage #
typescript
const localStorageMock = {
store: {} as Record<string, string>,
getItem(key: string) {
return this.store[key] || null
},
setItem(key: string, value: string) {
this.store[key] = value
},
removeItem(key: string) {
delete this.store[key]
},
clear() {
this.store = {}
}
}
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
})
最佳实践 #
1. 测试用户行为 #
typescript
it('should allow user to submit form', async () => {
await userEvent.type(emailInput, 'test@example.com')
await userEvent.click(submitButton)
expect(successMessage).toBeVisible()
})
2. 使用 data-testid #
html
<button data-testid="submit-btn">Submit</button>
<input data-testid="email-input">
3. 隔离测试 #
typescript
beforeEach(() => {
document.body.innerHTML = ''
vi.clearAllMocks()
})
4. 测试边界情况 #
typescript
it('should handle empty input', () => { })
it('should handle very long input', () => { })
it('should handle special characters', () => { })
小结 #
测试策略要点:
- 单元测试测试组件逻辑
- 集成测试测试组件交互
- E2E 测试测试完整流程
- 使用 Mock 隔离外部依赖
- 测试用户行为而非实现细节
至此,Alpine.js 完全指南全部完成!
最后更新:2026-03-28