Mengapa Menguji Route Middleware pada Level Unit?
Route middleware pada Nuxt 3 sering menjadi garis pertahanan pertama dalam sistem otentikasi dan otorisasi (RBAC). Kesalahan kecil saat refactor logika middleware dapat membuka celah keamanan di mana halaman privat dapat diakses oleh publik (access control regression).
Mengandalkan end-to-end (E2E) testing dengan browser nyata seperti Playwright atau Cypress untuk memverifikasi setiap kemungkinan matriks navigasi memakan waktu eksekusi yang lambat dan rentan flaky. Pengujian middleware pada unit level menggunakan @nuxt/test-utils/runtime dan Vitest memungkinkan eksekusi ratusan skenario navigasi dalam hitungan detik tanpa browser overhead.
Konfigurasi vitest.config.ts dengan Nuxt Environment
Nuxt menyediakan integrasi pengujian langsung ke runtime context-nya melalui @nuxt/test-utils. Pasang dependensi yang diperlukan terlebih dahulu:
npm install -D vitest @nuxt/test-utils happy-domBuat file vitest.config.ts di root proyek. Gunakan defineVitestConfig agar Vitest mengenali auto-imports, composables, dan alias Nuxt secara native:
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom'
}
},
globals: true,
clearMocks: true
}
})Implementasi Route Middleware
Perhatikan contoh route middleware middleware/auth.ts di bawah. Middleware ini memeriksa status otentikasi pengguna dan peran (role) target rute:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const { user, isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo({
path: '/login',
query: { redirect: to.fullPath }
})
}
const requiredRole = to.meta.requiredRole as string | undefined
if (requiredRole && user.value?.role !== requiredRole) {
return abortNavigation(
createError({
statusCode: 403,
statusMessage: 'Forbidden Access'
})
)
}
})Strategi Mocking Composable dan Navigasi
Gunakan mockNuxtImport dari @nuxt/test-utils/runtime untuk mengisolasi composable Nuxt. Hindari mock manual pada module import jika composable dimuat otomatis via Nuxt auto-import mechanism.
// tests/middleware/auth.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { RouteLocationNormalized } from 'vue-router'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
import authMiddleware from '~/middleware/auth'
const { useAuthMock } = vi.hoisted(() => ({
useAuthMock: vi.fn()
}))
mockNuxtImport('useAuth', () => useAuthMock)
describe('auth middleware', () => {
const createRoute = (meta = {}, fullPath = '/admin/dashboard'): RouteLocationNormalized => ({
fullPath,
path: fullPath,
query: {},
hash: '',
name: 'admin-dashboard',
params: {},
matched: [],
meta,
redirectedFrom: undefined
})
beforeEach(() => {
vi.clearAllMocks()
})
it('mengarahkan pengguna anonim ke halaman login beserta query redirect', async () => {
useAuthMock.mockReturnValue({
isAuthenticated: ref(false),
user: ref(null)
})
const to = createRoute({}, '/admin/settings')
const from = createRoute({}, '/')
const result = await authMiddleware(to, from)
expect(result).toEqual(navigateTo({
path: '/login',
query: { redirect: '/admin/settings' }
}))
})
it('menghentikan navigasi dengan status code 403 jika role tidak sesuai', async () => {
useAuthMock.mockReturnValue({
isAuthenticated: ref(true),
user: ref({ id: '1', role: 'viewer' })
})
const to = createRoute({ requiredRole: 'admin' })
const from = createRoute()
const result = await authMiddleware(to, from)
expect(result).toBeDefined()
// abortNavigation mengembalikan error instance atau NuxtError object
expect(result?.statusCode).toBe(403)
expect(result?.statusMessage).toBe('Forbidden Access')
})
it('mengizinkan navigasi jika user memiliki kredensial dan role yang valid', async () => {
useAuthMock.mockReturnValue({
isAuthenticated: ref(true),
user: ref({ id: '1', role: 'admin' })
})
const to = createRoute({ requiredRole: 'admin' })
const from = createRoute()
const result = await authMiddleware(to, from)
// Middleware Nuxt yang mengizinkan rute mengembalikan undefined
expect(result).toBeUndefined()
})
})Pengujian Global Middleware vs Named Middleware
Terdapat perbedaan mekanik saat menguji kedua varian middleware:
- Named Middleware: Dapat diimpor langsung sebagai fungsi standar (seperti contoh di atas) dan dieksekusi dengan argumen
(to, from)tiruan. Pendekatan ini paling terisolasi dan cepat. - Global Middleware: File dengan format
*.global.tsakan dieksekusi pada setiap navigasi secara otomatis di dalam aplikasi. Untuk mengujinya secara unit, tetap panggil fungsinya secara langsung. Namun, dalam integration test menggunakanregisterEndpointatau helper routing, pastikan middleware global tidak menimbulkan efek samping pada pengujian komponen lain.
Gunakanvi.resetModules()atauvi.clearAllMocks()di hookbeforeEach()untuk memastikan data reaktif (sepertiuseStateatau Pinia state) tidak bocor ke suite pengujian berikutnya (test pollution).
Integrasi CI Script
Jalankan pengujian unit ini pada pipeline CI tanpa perlu menginstal dependensi browser sistem operasi (seperti browser binary Playwright). Tambahkan script pada package.json:
{
"scripts": {
"test:unit": "vitest run"
}
}Snippet konfigurasi pada GitHub Actions workflow:
- name: Run Middleware Tests
run: npm run test:unitPengujian middleware via runtime environment Vitest memverifikasi routing logic secara instan, menghemat durasi CI pipeline, dan menjaga integritas kontrol akses sebelum kode sampai ke tahap deployment.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!