Giriş
Bir uygulama geliştirdiniz, her şey harika çalışıyor, production'a deploy ettiniz ve... bir kullanıcı formu doldurunca sayfa çöktü. Tanıdık geldi mi? Eğer geldiyse yalnız değilsiniz. Test yazmak, geliştiricilerin çoğunun "sonra yazarım" deyip bir daha asla dönmediği o meşhur adım. Ama açıkçası, 2026'da Next.js App Router ile geliştirme yapıyorsanız, test yazmamak artık lüks değil — doğrudan risk.
React Server Components, Server Actions, streaming rendering, Route Handlers... Bu yenilikler test stratejilerini kökten değiştirdi. Artık her bileşeni jsdom'da render edip "testi geçti" demek yeterli değil. Sunucu tarafında çalışan async bileşenlerin, form mutasyonlarının ve API endpoint'lerinin hepsini test etmeniz gerekiyor.
Bu rehberde, Next.js App Router projelerinizi sıfırdan nasıl test edeceğinizi adım adım göstereceğim. Vitest ile birim testleri, React Testing Library ile bileşen testleri ve Playwright ile uçtan uca (E2E) testler yazacağız. Daha önce React Server Components ve Veri Çekme veya Server Actions rehberlerimizi okuduysanız, bu makalede o kalıpları güvenilir testlerle nasıl pekiştireceğinizi de göreceksiniz.
2026'da Next.js Test Stratejisi: Büyük Resim
Hemen söyleyeyim: modern Next.js uygulamalarında test yazmak, tek bir araçla çözülebilecek bir iş değil. Farklı katmanlar için farklı araçlara ihtiyacınız var:
- Birim testleri (Unit tests): Tek bir fonksiyon veya yardımcı modülü izole ederek test eder. Vitest burada devreye giriyor.
- Bileşen testleri (Component tests): React bileşenlerinin doğru render olduğunu ve kullanıcı etkileşimlerine yanıt verdiğini kontrol eder. React Testing Library bunun için biçilmiş kaftan.
- Entegrasyon testleri (Integration tests): Birden fazla bileşenin, context'in ve API mock'larının birlikte çalıştığını doğrular.
- Uçtan uca testler (E2E tests): Gerçek bir tarayıcıda, gerçek kullanıcı senaryolarını simüle eder. Playwright bu katmanın tartışmasız lideri.
Test piramidi yerine test kupası: React uygulamalarında en yüksek değeri aslında entegrasyon testleri sağlıyor. Her bir fonksiyonu ayrı ayrı test etmek yerine, bileşen + context + API mock'ları birlikte test etmek hem daha az kırılgan hem de daha güvenilir sonuçlar veriyor. Bunu kendi projelerimde deneyimledikten sonra net söyleyebilirim — fark gerçekten belirgin.
Server Components Hakkında Önemli Bir Not
Async Server Components React ekosistemine görece yeni girdiği için, Vitest ve Jest bu bileşenleri henüz doğrudan desteklemiyor. Senkron Server ve Client Component'ler için birim testi yazabilirsiniz; ancak async bileşenler söz konusu olduğunda E2E testleri kullanmanız gerekiyor. Merak etmeyin, rehberde her iki yaklaşımı da göstereceğim.
Vitest Kurulumu ve Yapılandırması
Vitest, Vite tarafından desteklenen ve 2026 itibarıyla Jest'in yerini büyük ölçüde almış olan ultra hızlı bir test framework'ü. Vite'ın dönüşümlerini ve modül çözümleyicisini doğrudan kullandığı için ekstra yapılandırmayla uğraşmanıza gerek kalmıyor.
Paketlerin Kurulumu
Öncelikle gerekli paketleri dev dependency olarak yükleyin:
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event vite-tsconfig-paths
Her paketin ne işe yaradığına kısaca bakalım:
vitest— Test runner ve assertion kütüphanesi@vitejs/plugin-react— JSX dönüşümü için Vite eklentisijsdom— Tarayıcı ortamını simüle eder@testing-library/react— React bileşenlerini test etmek için@testing-library/jest-dom—toBeInTheDocument()gibi DOM matcher'ları ekler@testing-library/user-event— Gerçekçi kullanıcı etkileşimleri simüle edervite-tsconfig-paths— TypeScript path alias'larını çözümler
vitest.config.ts Dosyası
Proje kök dizininde bir vitest.config.ts dosyası oluşturun:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
include: ['**/*.test.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'.next/',
'**/*.config.*',
'**/*.d.ts',
],
},
},
});
Setup Dosyası
Her testten sonra temizlik yapacak bir vitest.setup.ts dosyasına ihtiyacımız var:
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
import '@testing-library/jest-dom/vitest';
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
Bu setup dosyası üç kritik işi hallediyor: her testten sonra DOM'u temizliyor, mock'ları sıfırlıyor ve jest-dom matcher'larını Vitest'e entegre ediyor. Küçük bir dosya ama atlamayın — atlarsanız testler birbirini etkilemeye başlar ve hata bulmak kabus olur.
package.json Script'leri
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
npm run test komutu Vitest'i watch modunda başlatır — dosyalarınızı kaydettiğinizde ilgili testler otomatik olarak yeniden çalışır. test:run tek seferlik çalıştırma için, test:coverage ise kapsam raporu üretmek için kullanılır.
Client Component Testleri
Client Component'ler, kullanıcı etkileşimi içeren bileşenlerdir ve React Testing Library ile test edilmeleri en doğal yol. Hadi bir örnekle başlayalım.
Basit Bir Bileşeni Test Etme
Diyelim ki basit bir sayaç bileşeniniz var:
// src/components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount);
return (
Sayaç: {count}
);
}
Bu bileşenin testi şöyle görünecek:
// src/components/__tests__/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { Counter } from '../Counter';
describe('Counter', () => {
it('varsayılan değerle render edilmeli', () => {
render( );
expect(screen.getByTestId('count')).toHaveTextContent('Sayaç: 0');
});
it('başlangıç değeriyle render edilmeli', () => {
render( );
expect(screen.getByTestId('count')).toHaveTextContent('Sayaç: 5');
});
it('artır butonuna tıklanınca değer artmalı', async () => {
const user = userEvent.setup();
render( );
await user.click(screen.getByText('Artır'));
expect(screen.getByTestId('count')).toHaveTextContent('Sayaç: 1');
await user.click(screen.getByText('Artır'));
expect(screen.getByTestId('count')).toHaveTextContent('Sayaç: 2');
});
it('sıfırla butonu değeri sıfırlamalı', async () => {
const user = userEvent.setup();
render( );
await user.click(screen.getByText('Sıfırla'));
expect(screen.getByTestId('count')).toHaveTextContent('Sayaç: 0');
});
});
Form Bileşeni Test Etme
Gerçek dünya uygulamalarında formlar muhtemelen en sık test edeceğiniz bileşenler olacak. İşte bir arama formu örneği:
// src/components/SearchForm.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function SearchForm() {
const [query, setQuery] = useState('');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
router.push(`/search?q=${encodeURIComponent(query)}`);
}
};
return (
);
}
Bu bileşende next/navigation modülünü mock'lamak gerekiyor. Bu kısım biraz can sıkıcı ama bir kez alıştığınızda çok kolay:
// src/components/__tests__/SearchForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { SearchForm } from '../SearchForm';
const mockPush = vi.fn();
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
replace: vi.fn(),
back: vi.fn(),
}),
}));
describe('SearchForm', () => {
it('arama formu render edilmeli', () => {
render( );
expect(screen.getByRole('search')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Arama yapın...')).toBeInTheDocument();
});
it('arama yapıldığında doğru URL ye yönlendirmeli', async () => {
const user = userEvent.setup();
render( );
await user.type(screen.getByLabelText('Arama'), 'next.js test');
await user.click(screen.getByText('Ara'));
expect(mockPush).toHaveBeenCalledWith(
'/search?q=next.js%20test'
);
});
it('boş sorgu ile yönlendirme yapmamalı', async () => {
const user = userEvent.setup();
render( );
await user.click(screen.getByText('Ara'));
expect(mockPush).not.toHaveBeenCalled();
});
});
Next.js Modüllerini Mock'lama
Next.js App Router'da test yazarken en sık karşılaşacağınız sorun, framework'e özgü modülleri mock'lamak olacak. Şunu kabul edelim — bu kısım biraz zahmetli. Ama bir kez hazır şablonlarınız oldu mu, sonraki testlerde kopyala-yapıştır yeterli.
İşte en yaygın senaryolar:
next/navigation Mock'lama
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
prefetch: vi.fn(),
}),
useSearchParams: () => new URLSearchParams('q=test'),
usePathname: () => '/dashboard',
useParams: () => ({ id: '123' }),
redirect: vi.fn(),
}));
next/image Mock'lama
vi.mock('next/image', () => ({
default: (props: React.ImgHTMLAttributes) => {
// eslint-disable-next-line @next/next/no-img-element
return
;
},
}));
next/headers Mock'lama
vi.mock('next/headers', () => ({
cookies: () => ({
get: vi.fn().mockReturnValue({ value: 'mock-token' }),
set: vi.fn(),
delete: vi.fn(),
}),
headers: () => new Headers({ 'content-type': 'application/json' }),
}));
Server Actions Test Etme
Server Actions, sunucu tarafında çalışan form mutasyonları. Bunları test etmek düşündüğünüz kadar zor değil aslında — action fonksiyonunu doğrudan import edip çağırabilirsiniz.
Diyelim ki bir görev ekleme action'ınız var:
// src/actions/todo.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { z } from 'zod';
const TodoSchema = z.object({
title: z.string().min(1, 'Başlık zorunludur').max(100),
});
export async function addTodo(formData: FormData) {
const rawData = {
title: formData.get('title'),
};
const validated = TodoSchema.safeParse(rawData);
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
await db.todo.create({
data: { title: validated.data.title },
});
revalidatePath('/todos');
return { success: true };
}
Ve bu action'ın testi:
// src/actions/__tests__/todo.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { addTodo } from '../todo';
// Next.js modüllerini mock la
vi.mock('next/cache', () => ({
revalidatePath: vi.fn(),
}));
// Veritabanı modülünü mock la
vi.mock('@/lib/db', () => ({
db: {
todo: {
create: vi.fn().mockResolvedValue({ id: 1, title: 'Test Todo' }),
},
},
}));
describe('addTodo', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('geçerli veri ile görev oluşturmalı', async () => {
const formData = new FormData();
formData.set('title', 'Yeni görev');
const result = await addTodo(formData);
expect(result).toEqual({ success: true });
});
it('boş başlık ile hata döndürmeli', async () => {
const formData = new FormData();
formData.set('title', '');
const result = await addTodo(formData);
expect(result.error).toBeDefined();
expect(result.error?.title).toBeDefined();
});
it('başlık 100 karakterden uzunsa hata döndürmeli', async () => {
const formData = new FormData();
formData.set('title', 'a'.repeat(101));
const result = await addTodo(formData);
expect(result.error).toBeDefined();
});
});
Dikkat ederseniz next/cache ve veritabanı katmanını mock'luyoruz. Böylece action'ın asıl iş mantığını (validasyon, hata yönetimi gibi) izole bir şekilde test edebiliyoruz. Bu yaklaşım hem hızlı hem de güvenilir sonuçlar veriyor.
Route Handlers Test Etme
Next.js App Router'daki Route Handlers, GET, POST, PUT, DELETE gibi HTTP metotlarını export eden fonksiyonlar. Bunları test etmek aslında oldukça kolay — doğrudan import edip bir Request nesnesi geçirmeniz yeterli.
// src/app/api/todos/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET() {
const todos = await db.todo.findMany({
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(todos);
}
export async function POST(request: Request) {
const body = await request.json();
if (!body.title || body.title.trim() === '') {
return NextResponse.json(
{ error: 'Başlık zorunludur' },
{ status: 400 }
);
}
const todo = await db.todo.create({
data: { title: body.title },
});
return NextResponse.json(todo, { status: 201 });
}
Test dosyası da şöyle:
// src/app/api/todos/__tests__/route.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST } from '../route';
const mockTodos = [
{ id: 1, title: 'Görev 1', createdAt: new Date() },
{ id: 2, title: 'Görev 2', createdAt: new Date() },
];
vi.mock('@/lib/db', () => ({
db: {
todo: {
findMany: vi.fn().mockResolvedValue(mockTodos),
create: vi.fn().mockImplementation(({ data }) =>
Promise.resolve({ id: 3, ...data, createdAt: new Date() })
),
},
},
}));
describe('GET /api/todos', () => {
it('tüm görevleri döndürmeli', async () => {
const response = await GET();
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(2);
expect(data[0].title).toBe('Görev 1');
});
});
describe('POST /api/todos', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('yeni görev oluşturmalı', async () => {
const request = new Request('http://localhost/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Yeni görev' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.title).toBe('Yeni görev');
});
it('başlık olmadan 400 döndürmeli', async () => {
const request = new Request('http://localhost/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '' }),
});
const response = await POST(request);
expect(response.status).toBe(400);
});
});
Playwright ile Uçtan Uca (E2E) Testler
Birim ve bileşen testleri kodun ayrı parçalarının doğru çalıştığını garanti eder, ama kullanıcının gerçek deneyimini tam olarak simüle edemez. İşte Playwright burada sahneye çıkıyor. Chromium, Firefox ve WebKit tarayıcılarını tek bir API ile otomasyon altına alır ve gerçek kullanıcı senaryolarını test etmenize olanak tanır.
Playwright Kurulumu
npm init playwright@latest
Bu komut sizi birkaç adımdan geçirecek: testlerin nereye yazılacağını, GitHub Actions CI yapılandırmasını ve tarayıcı indirmelerini ayarlayacaksınız. İşlem birkaç dakika sürebilir (tarayıcı indirmeleri biraz yer kaplıyor).
Playwright Yapılandırması
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
Burada dikkat etmeniz gereken birkaç şey var:
webServeryapılandırması testlerden önce dev sunucusunu otomatik başlatır — bunu manuel yapmanıza gerek yokfullyParalleltestleri paralel çalıştırarak süreyi ciddi ölçüde kısaltırtrace: 'on-first-retry'başarısız testlerin detaylı izlemesini sağlar (bu özellik hayat kurtarıcı)- Birden fazla tarayıcı ve mobil cihaz profili tanımlanmış
E2E Test Örnekleri
// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Navigasyon', () => {
test('ana sayfa doğru render edilmeli', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
await expect(page).toHaveTitle(/Next.js/);
});
test('sayfalar arası geçiş çalışmalı', async ({ page }) => {
await page.goto('/');
// Hakkımızda sayfasına git
await page.click('text=Hakkımızda');
await page.waitForURL('/about');
await expect(page).toHaveURL('/about');
// Geri dön
await page.goBack();
await expect(page).toHaveURL('/');
});
});
Form ve Veri Akışı E2E Testi
// e2e/todo-flow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Görev Yönetimi Akışı', () => {
test('yeni görev eklenebilmeli', async ({ page }) => {
await page.goto('/todos');
// Formu doldur
await page.fill('input[name="title"]', 'E2E test görevi');
await page.click('button[type="submit"]');
// Görevin listede görünmesini bekle
await expect(
page.locator('text=E2E test görevi')
).toBeVisible();
});
test('boş görev eklenememeli', async ({ page }) => {
await page.goto('/todos');
// Boş formla gönder
await page.click('button[type="submit"]');
// Hata mesajının göründüğünü doğrula
await expect(
page.locator('text=Başlık zorunludur')
).toBeVisible();
});
});
Async Server Component'leri E2E ile Test Etme
Daha önce belirttiğim gibi, async Server Component'leri Vitest ile doğrudan test edemezsiniz. İşte Playwright'ın bu boşluğu nasıl doldurduğunu gösteren bir örnek:
// e2e/server-component.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Sunucu Bileşeni: Ürünler Sayfası', () => {
test('ürünler sunucudan yüklenmeli', async ({ page }) => {
await page.goto('/products');
// Loading state in kaybolmasını bekle
await expect(page.locator('text=Yükleniyor...')).toBeHidden();
// Ürünlerin render edildiğini doğrula
const productCards = page.locator('[data-testid="product-card"]');
await expect(productCards.first()).toBeVisible();
// En az bir ürün olmalı
const count = await productCards.count();
expect(count).toBeGreaterThan(0);
});
test('streaming ile içerik aşamalı yüklenmeli', async ({ page }) => {
await page.goto('/dashboard');
// İlk bölüm hızlıca yüklenmeli
await expect(
page.locator('[data-testid="quick-stats"]')
).toBeVisible({ timeout: 2000 });
// Yavaş bölüm biraz daha sonra gelecek
await expect(
page.locator('[data-testid="detailed-analytics"]')
).toBeVisible({ timeout: 10000 });
});
});
Testleri CI/CD Pipeline'a Entegre Etme
Testlerin gerçek değeri, her commit'te otomatik olarak çalıştırıldığında ortaya çıkar. Lokalde "çalışıyor" demek güzel, ama asıl güvence CI'da. GitHub Actions ile hem birim testlerini hem de E2E testlerini otomatik çalıştırabilirsiniz:
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:run
- run: npm run test:coverage
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Test Yazarken En Sık Yapılan Hatalar
Next.js App Router projesinde test yazarken bazı tuzaklara dikkat etmeniz gerekiyor. Bunları kendi tecrübemden biliyorum — hepsine en az bir kere düştüm.
1. Uygulama Detaylarını Test Etme
Yanlış yaklaşım: State değişkeninin değerini doğrudan kontrol etmek.
Doğru yaklaşım: Kullanıcının gördüğü çıktıyı test etmek. React Testing Library'nin felsefesi tam olarak bu — "kullanıcıların gördüğünü test et."
// ❌ Kötü: Uygulama detayını test ediyor
expect(component.state.isOpen).toBe(true);
// ✅ İyi: Kullanıcının gördüğünü test ediyor
expect(screen.getByRole('dialog')).toBeVisible();
2. Erişilebilirlik Odaklı Sorgular Kullanmamak
// ❌ Kötü: Test ID ye bağımlı
screen.getByTestId('submit-btn');
// ✅ İyi: Erişilebilirlik rolleri kullan
screen.getByRole('button', { name: 'Gönder' });
Test ID'ler son çare olmalı. Önce getByRole, getByLabelText, getByPlaceholderText gibi erişilebilirlik odaklı sorguları deneyin. Hem testleriniz daha anlamlı olur hem de erişilebilirlik sorunlarını erken yakalamanıza yardımcı olur.
3. Her Şeyi Mock'lamak
Çok fazla mock kullanmak, testlerinizi gerçek uygulamadan koparır. Benim kuralım şu: ağ sınırlarını (veritabanı, dış API'ler) mock'layın, ancak kendi kodunuz arasındaki etkileşimleri mock'lamaktan kaçının. Aşırı mock'lama yapıyorsanız, muhtemelen testleriniz size yanlış bir güven hissi veriyor.
4. Async İşlemleri Doğru Beklememek
// ❌ Kötü: Elementi hemen arıyor, bulamayabilir
expect(screen.getByText('Yüklendi')).toBeInTheDocument();
// ✅ İyi: Elementin görünmesini bekliyor
expect(await screen.findByText('Yüklendi')).toBeInTheDocument();
Bu hata özellikle yeni başlayanların çok sık yaptığı bir şey. Async bir işlem varsa (API çağrısı, state güncellemesi vb.), mutlaka findBy veya waitFor kullanın.
Test Dosya Yapısı Önerisi
Proje büyüdükçe test dosyalarınızı düzenli tutmak giderek daha önemli hale geliyor. İşte önerdiğim yapı:
src/
├── components/
│ ├── Counter.tsx
│ ├── SearchForm.tsx
│ └── __tests__/
│ ├── Counter.test.tsx
│ └── SearchForm.test.tsx
├── actions/
│ ├── todo.ts
│ └── __tests__/
│ └── todo.test.ts
├── app/
│ └── api/
│ └── todos/
│ ├── route.ts
│ └── __tests__/
│ └── route.test.ts
├── lib/
│ ├── utils.ts
│ └── __tests__/
│ └── utils.test.ts
e2e/
├── navigation.spec.ts
├── todo-flow.spec.ts
└── server-component.spec.ts
vitest.config.ts
vitest.setup.ts
playwright.config.ts
Bu yapıda birim ve bileşen testleri kaynak kodun yanında __tests__ klasörlerinde yer alırken, E2E testleri proje kökündeki e2e/ klasöründe yaşıyor. Böylece hangi testin nereye ait olduğu her zaman net.
Sık Sorulan Sorular
Next.js App Router'da Vitest mi yoksa Jest mi kullanmalıyım?
2026 itibarıyla Vitest yeni Next.js projeleri için standart tercih haline geldi. Vitest, Vite'ın altyapısını kullandığı için çok daha hızlı çalışıyor, ESM desteği doğal olarak geliyor ve yapılandırması da daha basit. Jest hâlâ çalışır tabi, ama yeni bir proje başlıyorsanız kesinlikle Vitest ile gidin. Mevcut Jest projenizi acilen göç ettirmenize gerek yok — ama eninde sonunda geçiş yapmayı düşünmenizi öneririm.
Async Server Component'leri nasıl test edebilirim?
Async Server Component'ler henüz Vitest ve Jest tarafından doğrudan desteklenmiyor. İki strateji izleyebilirsiniz: birincisi, veri çekme mantığını ayrı bir fonksiyona çıkarıp o fonksiyonu birim testiyle test edin. İkincisi, bileşenin tamamını Playwright gibi bir E2E test aracıyla gerçek tarayıcıda test edin. Bu iki katmanlı yaklaşım hem iş mantığınızı hem de kullanıcı deneyimini güvence altına alır.
Server Actions testlerinde gerçek veritabanı mı kullanmalıyım yoksa mock mu?
Kısa cevap: ikisi de. Birim testlerinde veritabanını mock'layın — bu testlerin hızlı ve izole çalışması gerekiyor. Entegrasyon veya E2E testlerinde ise tercihen gerçek bir test veritabanı (mesela Docker içinde çalışan bir PostgreSQL) kullanın. Mock'lar hız sağlar, gerçek veritabanı testleri ise production'daki davranışı çok daha doğru yansıtır.
Playwright testlerini ne zaman çalıştırmalıyım?
E2E testleri birim testlerinden çok daha yavaş, bu yüzden her dosya kaydında çalıştırmak pek pratik değil. Önerim şu: geliştirme sırasında Vitest'i watch modunda çalıştırın, commit öncesinde kritik E2E testlerini bir geçirin ve CI/CD pipeline'ında tüm E2E test takımını çalıştırın. GitHub Actions'da her pull request'te otomatik E2E testi çalıştırmak en sağlam yaklaşım.
Test coverage hedefim ne olmalı?
Sabit bir yüzde hedeflemek yerine, kritik iş mantığının %100 kapsandığından emin olun. Genel bir kural olarak %80 civarı kapsam iyi bir hedeftir, ama önemli olan hangi kodun kapsandığı. Validasyon mantığı, ödeme akışları ve kimlik doğrulama gibi kritik alanlar mutlaka test edilmeli. Öte yandan, basit UI wrapper bileşenleri için test yazmak çoğu zaman gereksiz bir iş.