乐闻世界logo
搜索文章和话题

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

2月21日 15:18

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