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 Request dari pustaka bawaan sering kali memperlakukan header sebagai immutable setelah instansiasi selesai.
  • Abstraksi nextUrl pada NextRequest mem-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 url pada objek response saat melakukan rewrite. Next.js menyimpan instruksi rewrite pada header x-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 menginisialisasi NextRequest. Konstruktor NextRequest akan melempar TypeError: Invalid URL jika path relatif diberikan tanpa base URL.
  • Cegah Memory Leak Antar-Worker: Gunakan opsi restoreMocks: true pada konfigurasi Vitest. Jika Anda menggunakan vi.stubGlobal untuk mengubah env var (seperti process.env.NEXT_PUBLIC_APP_URL), pastikan untuk memulihkannya menggunakan blok afterEach.

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.