Menguji Next.js Middleware sering kali dialihkan ke pengujian End-to-End (E2E) menggunakan Playwright atau Cypress. Pendekatan ini memperlambat siklus feedback di pipeline CI/CD karena mewajibkan build aplikasi dan eksekusi HTTP server penuh. Solusi yang lebih efisien adalah menguji fungsi middleware.ts secara langsung sebagai unit/integration test menggunakan Vitest.
Tantangan utama pendekatan ini terletak pada runtime mismatch: Next.js Middleware berjalan di atas V8 Edge Runtime (berbasis standar Web API), sedangkan test runner Vitest berjalan di atas lingkungan Node.js standar. Artikel ini menguraikan cara mengisolasi dependensi runtime, membuat mock factory untuk NextRequest, dan melakukan assertion deterministik terhadap redirect dan rewrite.
Akar Masalah: Edge Runtime vs Node.js Environment
Middleware Next.js mengeksekusi logika jaringan menggunakan Web Standard APIs (seperti Request, Response, Headers, dan URL), namun diperluas melalui class internal Next.js seperti NextRequest dan NextResponse. Di Node.js standar:
- Objek
Requestdari pustaka bawaan sering kali memperlakukan header sebagai immutable setelah instansiasi selesai. - Abstraksi
nextUrlpadaNextRequestmem-parsing URL secara dinamis dengan mengandalkan header forwarded bawaan Next.js. - Penyimpanan cookie (
req.cookies) memerlukan parsing format RFC 6265 yang terintegrasi secara internal di Edge Runtime.
Jika Anda menguji middleware(req) secara langsung di Vitest tanpa inisialisasi context yang tepat, manipulasi cookie atau header sering memicu error runtime atau menghasilkan respons undefined.
Implementasi: Middleware Auth dan Multi-Tenant Rewrite
Gunakan implementasi middleware berikut sebagai target pengujian. Middleware ini menangani dua skenario: proteksi rute /dashboard dan rewrite rute multi-tenant berdasarkan header.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const token = req.cookies.get('session-token')?.value;
// 1. Guard Auth: Redirect ke /login jika tidak memiliki token
if (pathname.startsWith('/dashboard')) {
if (!token) {
const loginUrl = new URL('/login', req.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl, 307);
}
}
// 2. Tenant Rewrite: Rewrite rute API berdasarkan x-tenant-id
const tenant = req.headers.get('x-tenant-id');
if (pathname.startsWith('/api') && tenant) {
const rewrittenUrl = new URL(`/_tenants/${tenant}${pathname}`, req.url);
return NextResponse.rewrite(rewrittenUrl);
}
// 3. Mutasi header downstream via NextResponse.next
const response = NextResponse.next({
request: {
headers: new Headers(req.headers),
},
});
if (token) {
response.headers.set('x-auth-status', 'authenticated');
}
return response;
}Konfigurasi Vitest dan Eliminasi Flaky Test
Eksekusi parallel worker di Vitest dapat menyebabkan polusi global scope jika Anda memodifikasi objek global fetch, Request, atau Headers. Pastikan Vitest menjalankan isolasi thread yang bersih.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
isolate: true,
restoreMocks: true,
clearMocks: true,
},
});Catatan: Hindari menetapkan
environment: 'jsdom'untuk pengujian middleware. Lingkungan Node.js murni dengan global Web API bawaan (Node 18+) jauh lebih dekat perilakunya dengan Edge Runtime dibanding simulasi browser DOM.
Membuat Helper NextRequest Mock
Kita memerlukan helper untuk membuat instance NextRequest yang valid tanpa perlu memicu dependency injection yang rumit. Gunakan implementasi resmi dari next/server dengan opsi konfigurasi lengkap:
// test/mock-request.ts
import { NextRequest } from 'next/server';
interface MockRequestOptions {
method?: string;
headers?: Record<string, string>;
cookies?: Record<string, string>;
}
export function createMockNextRequest(path: string, options: MockRequestOptions = {}): NextRequest {
const url = `https://example.com${path}`;
const headers = new Headers(options.headers || {});
// Format cookies menjadi format string header 'cookie'
if (options.cookies) {
const cookieHeader = Object.entries(options.cookies)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('; ');
headers.set('cookie', cookieHeader);
}
return new NextRequest(new Request(url, {
method: options.method || 'GET',
headers,
}));
}Unit Tests: Asserting Status 307 dan Header Rewrites
Next.js menangani operasi NextResponse.rewrite() secara khusus dengan menyematkan header internal x-middleware-rewrite yang berisi URL absolut tujuan. Begitu juga dengan mutasi request header menggunakan NextResponse.next({ request: { headers } }) yang menyematkan metadata pada header x-middleware-request-*.
// middleware.test.ts
import { describe, it, expect } from 'vitest';
import { middleware } from './middleware';
import { createMockNextRequest } from './test/mock-request';
describe('Next.js Middleware Edge Suite', () => {
it('harus redirect (307) unauthenticated request pada path /dashboard', () => {
const req = createMockNextRequest('/dashboard/analytics');
const res = middleware(req);
expect(res.status).toBe(307);
const locationHeader = res.headers.get('location');
expect(locationHeader).toBe('https://example.com/login?from=%2Fdashboard%2Fanalytics');
});
it('harus mengizinkan akses /dashboard jika cookie session-token valid', () => {
const req = createMockNextRequest('/dashboard/analytics', {
cookies: { 'session-token': 'valid-jwt-token' },
});
const res = middleware(req);
expect(res.status).toBe(200);
expect(res.headers.get('x-auth-status')).toBe('authenticated');
});
it('harus melakukan rewrite path /api dengan prefix tenant pada header x-middleware-rewrite', () => {
const req = createMockNextRequest('/api/v1/users', {
headers: { 'x-tenant-id': 'acme-corp' },
});
const res = middleware(req);
expect(res.status).toBe(200);
// NextResponse.rewrite mengatur header internal ini secara eksplisit
const rewriteHeader = res.headers.get('x-middleware-rewrite');
expect(rewriteHeader).toBe('https://example.com/_tenants/acme-corp/api/v1/users');
});
it('harus bypass route biasa tanpa mutasi path atau redirect', () => {
const req = createMockNextRequest('/about');
const res = middleware(req);
expect(res.status).toBe(200);
expect(res.headers.get('location')).toBeNull();
expect(res.headers.get('x-middleware-rewrite')).toBeNull();
});
});Debugging dan Praktik Terbaik
- Verifikasi Header Rewrite: Jangan mencari perubahan properti
urlpada objek response saat melakukan rewrite. Next.js menyimpan instruksi rewrite pada headerx-middleware-rewrite. Assertion harus selalu diarahkan ke header tersebut. - Gunakan URL Statis Lengkap pada Base Mock: Selalu sediakan origin absolut (misal
https://example.com) saat menginisialisasiNextRequest. KonstruktorNextRequestakan melemparTypeError: Invalid URLjika path relatif diberikan tanpa base URL. - Cegah Memory Leak Antar-Worker: Gunakan opsi
restoreMocks: truepada konfigurasi Vitest. Jika Anda menggunakanvi.stubGlobaluntuk mengubah env var (sepertiprocess.env.NEXT_PUBLIC_APP_URL), pastikan untuk memulihkannya menggunakan blokafterEach.
Menguji Next.js Middleware langsung pada unit runner memotong waktu eksekusi test suite dari satuan menit (pada level E2E) menjadi hitungan milidetik, tanpa mengorbankan akurasi verifikasi boundary logic perutean aplikasi Anda.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!