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:
-
Vitest (Recommended):
- Deep integration with Vite
- Fast test execution
- TypeScript support
-
Jest:
- Mature testing framework
- Rich ecosystem
- Widely used
-
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:
-
Testing Pyramid:
- Many unit tests
- Moderate integration tests
- Few end-to-end tests
-
Test Organization:
- Organize tests by feature
- Use clear test names
- Keep tests independent
-
Mock and Stub:
- Isolate external dependencies
- Use vi.mock() to mock modules
- Provide consistent test data
-
Continuous Integration:
- Run tests in CI
- Set test coverage thresholds
- Automate test reporting
-
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.