5月27日 15:38

How do you perform testing in Astro projects? How do you use testing frameworks like Vitest and Playwright?

Testing strategies in Astro are crucial for ensuring code quality and application stability. Understanding how to perform unit testing, integration testing, and end-to-end testing in Astro projects is an essential skill for developers.

Testing Framework Selection:

  1. Vitest (Recommended):

    • Deep integration with Vite
    • Fast test execution
    • TypeScript support
  2. Jest:

    • Mature testing framework
    • Rich ecosystem
    • Widely used
  3. Playwright:

    • End-to-end testing
    • Cross-browser support
    • Modern API

Installing Test Dependencies:

bash
# Install Vitest npm install -D vitest @vitest/ui # Install testing utilities npm install -D @testing-library/react @testing-library/vue @testing-library/svelte # Install Playwright npm install -D @playwright/test

Configuring Test Environment:

javascript
// vitest.config.ts import { defineConfig } from 'vitest/config'; import astro from 'astro/vitest'; export default defineConfig({ plugins: [astro()], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], }, });
typescript
// src/test/setup.ts import { expect, afterEach } from 'vitest'; import { cleanup } from '@testing-library/react'; // Clean up test environment afterEach(() => { cleanup(); }); // Extend expect expect.extend({});

Unit Testing Astro Components:

typescript
// src/components/__tests__/Button.astro.test.ts import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import Button from '../Button.astro'; describe('Button Component', () => { it('renders button with correct text', () => { const { getByText } = render(Button, { props: { text: 'Click me' }, }); expect(getByText('Click me')).toBeInTheDocument(); }); it('applies correct variant class', () => { const { container } = render(Button, { props: { variant: 'primary' }, }); const button = container.querySelector('button'); expect(button).toHaveClass('btn-primary'); }); });

Testing React Components:

typescript
// src/components/__tests__/Counter.test.tsx import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Counter from '../Counter'; describe('Counter Component', () => { it('renders initial count', () => { render(<Counter initialCount={0} />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('increments count when button is clicked', async () => { const user = userEvent.setup(); render(<Counter initialCount={0} />); const button = screen.getByRole('button', { name: 'Increment' }); await user.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); });

Testing Vue Components:

typescript
// src/components/__tests__/TodoList.test.ts import { describe, it, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import TodoList from '../TodoList.vue'; describe('TodoList Component', () => { it('renders todo items', () => { const todos = [ { id: 1, text: 'Learn Astro', completed: false }, { id: 2, text: 'Build app', completed: true }, ]; const wrapper = mount(TodoList, { props: { todos }, }); expect(wrapper.findAll('.todo-item')).toHaveLength(2); expect(wrapper.text()).toContain('Learn Astro'); }); it('emits complete event when checkbox is clicked', async () => { const wrapper = mount(TodoList, { props: { todos: [{ id: 1, text: 'Task', completed: false }] }, }); await wrapper.find('input[type="checkbox"]').setValue(true); expect(wrapper.emitted('complete')).toBeTruthy(); expect(wrapper.emitted('complete')[0]).toEqual([1]); }); });

Testing API Routes:

typescript
// src/pages/api/__tests__/users.test.ts import { describe, it, expect, beforeEach, vi } from 'vitest'; import { GET, POST } from '../users'; describe('Users API', () => { beforeEach(() => { vi.clearAllMocks(); }); it('GET returns list of users', async () => { const request = new Request('http://localhost/api/users'); const response = await GET({ request } as any); const data = await response.json(); expect(response.status).toBe(200); expect(data).toHaveProperty('users'); expect(Array.isArray(data.users)).toBe(true); }); it('POST creates new user', async () => { const userData = { name: 'John Doe', email: 'john@example.com' }; const request = new Request('http://localhost/api/users', { method: 'POST', body: JSON.stringify(userData), }); const response = await POST({ request } as any); const data = await response.json(); expect(response.status).toBe(201); expect(data).toHaveProperty('id'); expect(data.name).toBe(userData.name); }); });

Testing Content Collections:

typescript
// src/content/__tests__/blog.test.ts import { describe, it, expect } from 'vitest'; import { getCollection } from 'astro:content'; describe('Blog Content Collection', () => { it('has required frontmatter fields', async () => { const posts = await getCollection('blog'); posts.forEach(post => { expect(post.data).toHaveProperty('title'); expect(post.data).toHaveProperty('publishDate'); expect(post.data).toHaveProperty('description'); }); }); it('has valid publish dates', async () => { const posts = await getCollection('blog'); posts.forEach(post => { expect(post.data.publishDate).toBeInstanceOf(Date); expect(post.data.publishDate.getTime()).not.toBeNaN(); }); }); });

End-to-End Testing (Playwright):

typescript
// e2e/home.spec.ts import { test, expect } from '@playwright/test'; test.describe('Home Page', () => { test('loads successfully', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/My Astro App/); await expect(page.locator('h1')).toContainText('Welcome'); }); test('navigation works', async ({ page }) => { await page.goto('/'); await page.click('text=About'); await expect(page).toHaveURL(/\/about/); await expect(page.locator('h1')).toContainText('About Us'); }); test('form submission', async ({ page }) => { await page.goto('/contact'); await page.fill('input[name="name"]', 'John Doe'); await page.fill('input[name="email"]', 'john@example.com'); await page.fill('textarea[name="message"]', 'Hello!'); await page.click('button[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); }); });

Testing Middleware:

typescript
// src/middleware/__tests__/auth.test.ts import { describe, it, expect, vi } from 'vitest'; import { onRequest } from '../middleware'; describe('Auth Middleware', () => { it('redirects to login without token', async () => { const request = new Request('http://localhost/dashboard'); const redirectSpy = vi.fn(); await onRequest({ request, redirect: redirectSpy } as any); expect(redirectSpy).toHaveBeenCalledWith('/login'); }); it('allows access with valid token', async () => { const request = new Request('http://localhost/dashboard', { headers: { 'Authorization': 'Bearer valid-token' }, }); const nextSpy = vi.fn().mockResolvedValue(new Response()); await onRequest({ request, next: nextSpy } as any); expect(nextSpy).toHaveBeenCalled(); }); });

Test Configuration Scripts:

json
// package.json { "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed" } }

Test Coverage:

javascript
// vitest.config.ts import { defineConfig } from 'vitest/config'; import astro from 'astro/vitest'; export default defineConfig({ plugins: [astro()], test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'src/test/', '**/*.d.ts', '**/*.config.*', '**/mockData', ], }, }, });

Best Practices:

  1. Testing Pyramid:

    • Many unit tests
    • Moderate integration tests
    • Few end-to-end tests
  2. Test Organization:

    • Organize tests by feature
    • Use clear test names
    • Keep tests independent
  3. Mock and Stub:

    • Isolate external dependencies
    • Use vi.mock() to mock modules
    • Provide consistent test data
  4. Continuous Integration:

    • Run tests in CI
    • Set test coverage thresholds
    • Automate test reporting
  5. Test Performance:

    • Use test caching
    • Run tests in parallel
    • Optimize test execution time

Astro's testing ecosystem provides comprehensive testing support to help developers build reliable applications.

标签:Astro