Server Actions pada Next.js App Router mengeksekusi mutasi data langsung di sisi server. Namun, mengeksekusi Server Actions di luar siklus hidup HTTP Next.js—seperti di dalam test runner Node.js—sering kali memicu error runtime karena dependensi terhadap request context internal.
Mengandalkan end-to-end (E2E) testing seperti Playwright untuk setiap pengujian logika bisnis Server Action menimbulkan inefisiensi waktu eksekusi dan konsumsi resource CI yang besar. Solusi yang lebih terukur adalah mengisolasi dependensi runtime Next.js menggunakan Vitest.
Tantangan Runtime Next.js: Masalah 'next/headers'
Saat Server Action memanggil fungsi dari modul next/headers (seperti cookies() atau headers()), Next.js mengakses store internal berbasis AsyncLocalStorage. Jika fungsi tersebut dijalankan langsung di lingkungan Vitest tanpa inisialisasi server Next.js aktif, runtime akan melempar error:
Error: Dynamic server usage: cookies() was called outside a request scope.Untuk menguji Server Action secara terisolasi tanpa server Next.js aktif, store internal ini harus distub atau di-mock pada tingkat modul.
Implementasi Server Action Target
Berikut adalah Server Action tipikal yang menangani pembaruan profil pengguna. Fungsi ini memverifikasi token sesi melalui cookie, memvalidasi payload menggunakan Zod, dan mengeksekusi mutasi database.
// app/actions/update-profile.ts
'use server';
import { cookies } from 'next/headers';
import { z } from 'zod';
import { db } from '@/lib/db';
const ProfileSchema = z.object({
displayName: z.string().min(3).max(50),
bio: z.string().max(160).optional(),
});
export type UpdateProfileInput = z.infer<typeof ProfileSchema>;
export async function updateProfileAction(rawInput: unknown) {
// 1. Validasi Input
const parseResult = ProfileSchema.safeParse(rawInput);
if (!parseResult.success) {
return {
success: false,
error: 'VALIDATION_ERROR',
details: parseResult.error.flatten().fieldErrors,
};
}
// 2. Baca Cookie & Autentikasi
const cookieStore = await cookies();
const sessionToken = cookieStore.get('session_token')?.value;
if (!sessionToken) {
return { success: false, error: 'UNAUTHORIZED' };
}
const session = await db.session.findUnique({
where: { token: sessionToken },
select: { userId: true, expiresAt: new Date() },
});
if (!session || session.expiresAt < new Date()) {
return { success: false, error: 'UNAUTHORIZED' };
}
// 3. Mutasi Database
const updatedUser = await db.user.update({
where: { id: session.userId },
data: parseResult.data,
select: { id: true, displayName: true, bio: true },
});
return { success: true, data: updatedUser };
}Isolasi Cookie, Auth, dan Database via Vitest
Pengujian unit harus memverifikasi alur validasi, kegagalan autentikasi, dan keberhasilan mutasi secara independen dari infrastruktur jaringan atau database asli.
Konfigurasi Test Suite
Gunakan vi.mock untuk mengalihkan import next/headers dan client database:
// app/actions/__tests__/update-profile.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { updateProfileAction } from '../update-profile';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
vi.mock('next/headers', () => ({
cookies: vi.fn(),
}));
vi.mock('@/lib/db', () => ({
db: {
session: {
findUnique: vi.fn(),
},
user: {
update: vi.fn(),
},
},
}));
describe('updateProfileAction', () => {
const mockCookieGet = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(cookies).mockResolvedValue({
get: mockCookieGet,
} as any);
});
it('mengembalikan VALIDATION_ERROR jika input tidak sesuai schema', async () => {
const invalidInput = { displayName: 'ab' }; // min 3 karakter
const result = await updateProfileAction(invalidInput);
expect(result.success).toBe(false);
expect(result.error).toBe('VALIDATION_ERROR');
expect(db.user.update).not.toHaveBeenCalled();
});
it('mengembalikan UNAUTHORIZED jika session cookie tidak tersedia', async () => {
mockCookieGet.mockReturnValue(undefined);
const result = await updateProfileAction({ displayName: 'John Doe' });
expect(result.success).toBe(false);
expect(result.error).toBe('UNAUTHORIZED');
expect(db.session.findUnique).not.toHaveBeenCalled();
});
it('mengembalikan UNAUTHORIZED jika session token kedaluwarsa', async () => {
mockCookieGet.mockReturnValue({ value: 'valid-token' });
vi.mocked(db.session.findUnique).mockResolvedValue({
userId: 'usr_123',
expiresAt: new Date(Date.now() - 10000), // Kedaluwarsa
} as any);
const result = await updateProfileAction({ displayName: 'John Doe' });
expect(result.success).toBe(false);
expect(result.error).toBe('UNAUTHORIZED');
expect(db.user.update).not.toHaveBeenCalled();
});
it('berhasil memutasi data ketika input dan session valid', async () => {
mockCookieGet.mockReturnValue({ value: 'valid-token' });
vi.mocked(db.session.findUnique).mockResolvedValue({
userId: 'usr_123',
expiresAt: new Date(Date.now() + 60000),
} as any);
vi.mocked(db.user.update).mockResolvedValue({
id: 'usr_123',
displayName: 'John Doe',
bio: 'Software Engineer',
} as any);
const payload = { displayName: 'John Doe', bio: 'Software Engineer' };
const result = await updateProfileAction(payload);
expect(result.success).toBe(true);
expect(db.user.update).toHaveBeenCalledWith({
where: { id: 'usr_123' },
data: payload,
select: expect.any(Object),
});
expect(result.data?.displayName).toBe('John Doe');
});
});Trade-off: Unit Test Vitest vs E2E Playwright
Menentukan strategi testing Server Action bergantung pada prioritas feedback loop dan tingkat kepastian yang dibutuhkan:
- Execution Speed: Vitest mengeksekusi pengujian dalam rentang milidetik per file secara paralel in-process. Playwright membutuhkan proses bootstrap server Next.js, navigasi browser, dan rendering DOM yang membutuhkan hitungan detik.
- Fidelity Pipeline HTTP: Pengujian Vitest melewati middleware Next.js asli (seperti routing middleware dan validasi CSRF header internal Next.js). Playwright menguji skenario end-to-end nyata dari payload browser form-data hingga respons HTTP 200/500.
- Resource CI: Vitest dapat berjalan pada runner standar dengan memori rendah tanpa binary browser. Playwright membutuhkan dependensi OS untuk browser engines (Chromium/WebKit).
Gunakan Vitest untuk menguji edge-case validasi Zod, authorization checks, dan database branch logic. Gunakan Playwright secukupnya (smoke test) untuk memastikan Server Action terpasang benar dengan form binding dan UI actions.
Integrasi CI Pipeline (GitHub Actions)
Jalankan unit test Server Action terisolasi pada setiap Pull Request untuk mencegah regresi logika mutasi data tanpa membebani kuota runner CI:
name: Unit & Integration Tests
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Run Vitest Suite
run: npx vitest run --coveragePendekatan ini memastikan seluruh validasi schema dan autorisasi pada Server Actions tervalidasi sebelum masuk ke tahap deployment, menjaga performa testing pipeline tetap di bawah hitungan menit.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!