Astro Testing: Unit Test และ E2E Test

#astro13 เม.ย. 2569

Astro Testing: Unit Test และ E2E Test

การทดสอบเป็นส่วนสำคัญของการพัฒนา software ที่มีคุณภาพ Astro รองรับการทดสอบหลายรูปแบบตั้งแต่ unit tests ไปจนถึง end-to-end tests บทความนี้จะแนะนำการตั้งค่าและเขียน tests สำหรับ Astro projects

ประเภทของ Tests

  • Unit Tests: ทดสอบ functions และ utilities แยกส่วน
  • Component Tests: ทดสอบ UI components
  • Integration Tests: ทดสอบการทำงานร่วมกันของหลาย components
  • E2E Tests: ทดสอบ user flows ทั้งหมด

Unit Testing ด้วย Vitest

ติดตั้ง Vitest:

npm install -D vitest @vitest/ui

แก้ไข package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

สร้าง vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/env.d.ts'],
    },
  },
});

เขียน Unit Tests

// src/utils/formatDate.ts
export function formatDate(date: Date | string, locale = 'th-TH'): string {
  const d = typeof date === 'string' ? new Date(date) : date;
  return d.toLocaleDateString(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
}

export function formatRelativeTime(date: Date | string): string {
  const d = typeof date === 'string' ? new Date(date) : date;
  const now = new Date();
  const diffMs = now.getTime() - d.getTime();
  const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

  if (diffDays === 0) return 'วันนี้';
  if (diffDays === 1) return 'เมื่อวาน';
  if (diffDays < 7) return `${diffDays} วันที่แล้ว`;
  if (diffDays < 30) return `${Math.floor(diffDays / 7)} สัปดาห์ที่แล้ว`;
  return formatDate(d);
}
// src/utils/formatDate.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { formatDate, formatRelativeTime } from './formatDate';

describe('formatDate', () => {
  it('formats date in Thai locale', () => {
    const date = new Date('2024-01-15');
    const result = formatDate(date);
    expect(result).toContain('2567'); // Thai year
  });

  it('accepts string date', () => {
    const result = formatDate('2024-06-01');
    expect(typeof result).toBe('string');
    expect(result.length).toBeGreaterThan(0);
  });

  it('formats with custom locale', () => {
    const date = new Date('2024-01-15');
    const result = formatDate(date, 'en-US');
    expect(result).toContain('2024');
  });
});

describe('formatRelativeTime', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2024-06-15'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('returns วันนี้ for today', () => {
    expect(formatRelativeTime(new Date('2024-06-15'))).toBe('วันนี้');
  });

  it('returns เมื่อวาน for yesterday', () => {
    expect(formatRelativeTime(new Date('2024-06-14'))).toBe('เมื่อวาน');
  });

  it('returns days ago for recent dates', () => {
    expect(formatRelativeTime(new Date('2024-06-12'))).toBe('3 วันที่แล้ว');
  });
});

Testing API Routes

// src/pages/api/posts.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock database
vi.mock('../../lib/db', () => ({
  getPosts: vi.fn(),
  createPost: vi.fn(),
}));

import { getPosts, createPost } from '../../lib/db';

describe('GET /api/posts', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('returns posts with pagination', async () => {
    const mockPosts = [
      { id: '1', title: 'Post 1', slug: 'post-1' },
      { id: '2', title: 'Post 2', slug: 'post-2' },
    ];

    vi.mocked(getPosts).mockResolvedValue(mockPosts as any);

    const request = new Request('http://localhost/api/posts?page=1');
    // Test your API handler
    expect(getPosts).toBeDefined();
  });
});

Component Testing ด้วย @testing-library

npm install -D @testing-library/dom @testing-library/user-event jsdom

อัปเดต vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';

E2E Testing ด้วย Playwright

ติดตั้ง Playwright:

npm install -D @playwright/test
npx playwright install

สร้าง playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:4321',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:4321',
    reuseExistingServer: !process.env.CI,
  },
});

เขียน E2E tests:

// e2e/blog.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Blog', () => {
  test('shows list of posts', async ({ page }) => {
    await page.goto('/blog');
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
    await expect(page.locator('article')).toHaveCount(9);
  });

  test('navigates to post detail', async ({ page }) => {
    await page.goto('/blog');
    const firstPost = page.locator('article').first();
    const title = await firstPost.locator('h2').textContent();
    await firstPost.locator('a').click();
    await expect(page.locator('h1')).toHaveText(title!);
  });

  test('search works', async ({ page }) => {
    await page.goto('/blog');
    await page.getByPlaceholder('ค้นหา').fill('Astro');
    await page.waitForResponse('/api/search*');
    const results = page.locator('.search-results a');
    await expect(results.first()).toBeVisible();
  });
});

test.describe('Contact Form', () => {
  test('submits successfully', async ({ page }) => {
    await page.goto('/contact');
    await page.getByLabel('ชื่อ').fill('ทดสอบ');
    await page.getByLabel('อีเมล').fill('test@example.com');
    await page.getByLabel('ข้อความ').fill('ข้อความทดสอบ');
    await page.getByRole('button', { name: 'ส่ง' }).click();
    await expect(page.getByText('ส่งสำเร็จ')).toBeVisible();
  });
});

สรุป

การทดสอบ Astro projects ด้วย Vitest สำหรับ unit tests และ Playwright สำหรับ E2E tests ทำให้มั่นใจได้ว่า application ทำงานถูกต้อง การเขียน tests ที่ดีช่วยลด bugs, เพิ่มความมั่นใจในการ refactor, และทำให้ codebase มีคุณภาพสูงขึ้น