ข้ามไปเนื้อหาหลัก

Category: guide

Vitest — Unit Testing สำหรับ TypeScript และ Vite Projects

Vitest คือ test runner ที่เร็วที่สุดสำหรับ Vite-based projects — setup วิธีเขียน test และ pattern ที่ใช้จริง

· อ่านประมาณ 7 นาที

สารบัญ

ทำไม Vitest

Vitest ใช้ Vite เป็น engine จึงเร็วมาก รองรับ TypeScript ในตัว, ESM native, และใช้ Jest-compatible API ทำให้ย้ายโค้ดทดสอบเดิมได้โดยแทบไม่แก้ไข

npm install -D vitest

Setup

// package.json
{
  "scripts": {
    "test":    "vitest",
    "test:ui": "vitest --ui",
    "test:run":"vitest run"
  }
}
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',   // หรือ 'jsdom' สำหรับ DOM testing
    globals: true,         // ใช้ describe/it/expect โดยไม่ต้อง import
  },
});

เขียน Test แรก

// src/utils/math.ts
export function add(a: number, b: number): number {
  return a + b;
}
// src/utils/math.test.ts
import { describe, it, expect } from 'vitest';
import { add } from './math';

describe('add()', () => {
  it('บวกตัวเลขสองตัวถูกต้อง', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('บวกค่าลบได้', () => {
    expect(add(-1, 1)).toBe(0);
  });
});

Matchers ที่ใช้บ่อย

expect(value).toBe(5)                    // strict equal (===)
expect(value).toEqual({ a: 1 })          // deep equal
expect(value).toBeTruthy()               // truthy
expect(value).toBeNull()                 // null
expect(arr).toContain('item')            // array มี item
expect(str).toMatch(/pattern/)           // regex match
expect(fn).toThrow('error message')      // throw error
expect(fn).not.toThrow()                 // ไม่ throw

Async Testing

import { it, expect } from 'vitest';

it('fetch ข้อมูลสำเร็จ', async () => {
  const data = await fetchUser(1);
  expect(data.id).toBe(1);
  expect(data.name).toBeTruthy();
});

Mock Functions

import { vi, it, expect } from 'vitest';

it('เรียก callback เมื่อ submit', () => {
  const onSubmit = vi.fn();
  handleSubmit('data', onSubmit);
  expect(onSubmit).toHaveBeenCalledOnce();
  expect(onSubmit).toHaveBeenCalledWith('data');
});

Mock Module

vi.mock('./api', () => ({
  fetchPosts: vi.fn().mockResolvedValue([
    { id: 1, title: 'Test Post' },
  ]),
}));

DOM Testing กับ jsdom

// vitest.config.ts
test: { environment: 'jsdom' }
import { it, expect, beforeEach } from 'vitest';

beforeEach(() => {
  document.body.innerHTML = '<button id="btn">Click</button>';
});

it('button มี text ถูกต้อง', () => {
  const btn = document.getElementById('btn');
  expect(btn?.textContent).toBe('Click');
});

Test Coverage

npm install -D @vitest/coverage-v8

# vitest.config.ts
test: {
  coverage: {
    provider: 'v8',
    reporter: ['text', 'html'],
    include: ['src/**/*.ts'],
    exclude: ['src/**/*.test.ts'],
  },
}
vitest run --coverage

Vitest UI

npm install -D @vitest/ui
npx vitest --ui

เปิด browser ที่ http://localhost:51204/__vitest__/ จะเห็น test results แบบ visual พร้อม re-run และ filter


Pattern: test ตาม behavior ไม่ใช่ implementation

// ❌ test implementation
it('เรียก calculateTotal ครั้งเดียว', () => {
  expect(spy).toHaveBeenCalledOnce();
});

// ✓ test behavior
it('แสดงราคารวมที่ถูกต้อง', () => {
  expect(screen.getByText('฿250')).toBeTruthy();
});