
Thu Aug 13 2026

Hazırsan başlayalım 🚀
Yıllarca testler geçti mesajını gördüğümüzde "her şey yolunda" diye nefes alıp geçtik. Geçmişte testler biz insan geliştiriciler için bir güven ağıydı — refaktör yaparken bir şey kırıp kırmadığımızı anlayan, "şu an kod çalışıyor" diyen sessiz bir arkadaştı ✅
Ama şimdi durum değişti.
Artık kodumuzu okuyan, anlayan ve değiştiren bir AI ajanı da var ortada 🤖
AI Ajanları ile çalışırken kodumuz Agent-Ready olmalı — yani testler birer sözleşme gibi davranmalı 📜
Bu yazıda testleri sadece geçmek için değil, AI'ın anlayabileceği bir sözleşme haline getirmek için nasıl yazacağımızı keşfedeceğiz 🔍
Arkadaşlar, Yazılım Mimarisi ve Test Odaklı Geliştirme konularında uzun yıllar süren alışkanlıklarımızı sorgulamamız gereken bir noktadayız 🎯
İnsan bir testi okurken bağlamı (context) kafasında canlandırır:
createUser fonksiyonunu görür → "Kullanıcı oluşturma işlemi" derexpect(result).toBeTruthy() satırını okur → "İşlem başarılı olmuş" anlarAI ajanı ise? Sadece tokenlar, tipler ve yapı görür 🤖
"Test geçiyor" demek yetmez. Ajanın neyi test ettiğini, girdi/çıktıların ne olduğunu yapısal olarak bilmesi gerekir.
İşte aynı senaryo için yazılmış iki test — solda insana hitap eden, sağda AI ajanı dostu versiyon:
// ❌ İnsan odaklı — "Anlamlı" ama yapısal zayıf
test('Kullanıcı oluşturulduğunda başarılı dönmeli', async () => {
const result = await createUser({ name: 'Ahmet', email: 'ahmet@test.com' });
expect(result).toBeTruthy(); // Ne döndü? Bilinmiyor.
expect(result.id).toBeDefined(); // ID var mı? Tipi ne?
// "Başarılı" demek yetiyor insana 😅
});
// ✅ AI ajanı odaklı — Şema, tipler, kontrat net
test('createUser: valid input → returns UserResponse', async () => {
const input: CreateUserInput = { name: 'Ahmet', email: 'ahmet@test.com' };
const result = await createUser(input);
// Yapısal doğrulama — ajan bu şemayı *anlar*
expect(result).toMatchObject<UserResponse>({
id: expect.any(String), // UUID string
name: 'Ahmet', // Aynen girdi
email: 'ahmet@test.com', // Aynen girdi
createdAt: expect.any(String), // ISO date
isActive: true // Default değer
});
});
| Boyut | İnsan Odaklı Test | AI Ajanı Odaklı Test |
|---|---|---|
| Assertion | toBeTruthy() — boolean yeterli |
toMatchObject<Schema>() — yapı zorunlu |
| Girdi/Çıktı | İmplicit, isimden tahmin edilir | Explicit tip tanımı (CreateUserInput, UserResponse) |
| Bağlam | Kafada tutulur | Tokenlarda, tiplerde, şemada yer alır |
| Yorum satırı | "Başarılı olmalı" | Gereksiz — kod kendini anlatır 🛠 |
AI ajanları kod üretirken, refactor yaparken veya hata ayıklarken testlerinizi tek bilgi kaynağı olarak kullanır. Eğer testleriniz "boolean true döndü" diyorsa, ajan:
Şema doğrulamalı, girdi/çıkış tipi net testler yazmak — bu sadece "iyi pratik" değil, ajanlarla güvenli çalışmanın şartı 🔐
Hazırsan bir sonraki bölümde bu yapısal test yazımını nasıl sistematik hale getireceğimizi (şema kütüphaneleri, contract testing, type-safe test helpers) konuşalım 🚀
Hazırsan bu kavramı bir kahve molası süresinde bitirelim ☕️
Agent-Ready Code kısaca şu demek: Kodun, bir LLM/ajan tarafından baştan sona anlaşıp, güvenle değiştirilebilir ve genişletilebilir hale getirilmesi.
Eski dünyada "sözleşme" = fonksiyon imzasıydı. Yeni dünyada sözleşme çok daha fazlası 👇
Bu sözleşme tip güvenliğiyle zorunlu kılınıyor. TypeScript'in tür sistemi, sözleşmeyi derleme zamanında bir "kanıt"a çeviriyor. LLM kod üretirken prompt'a bu şemaları (veya Zod/JSON Schema karşılıklarını) besliyorsunuz; model "sözleşmeye uyan" kod yazmak zorunda kalıyor 🎯
Aşağıda bir fonksiyon için Sözleşme tanımını temsil eden TypeScript bloğu var. Kopyala-yapıştır değil, mental model olarak sakla:
// 📄 Sözleşme tanımı — tek kaynak gerçeği (Single Source of Truth)
interface FunctionContract<
TInput extends object,
TOutput extends object,
TErrorCode extends string,
TSideEffect extends string
> {
/** Girdi şeması — Zod/JSON Schema ile de ifade edilebilir */
inputSchema: TInput;
/** Çıktı şeması — başarı durumunda ne döner? */
outputSchema: TOutput;
/** Hata kodları — union type ile exhaustive check zorunlu kılınır */
errorCodes: readonly TErrorCode[];
/** Yan etkiler — sadece dokümantasyon değil, static analysis için de kaynak */
sideEffects: readonly TSideEffect[];
/** Performans SLA'si — opsiyonel ama önerilir */
performance?: {
latencyP95Ms: number;
maxThroughputPerSec?: number;
retryPolicy?: {
maxAttempts: number;
backoffMs: number;
};
};
}
// 🎯 Kullanım örneği: "Kullanıcı oluştur" fonksiyonu sözleşmesi
type CreateUserContract = FunctionContract<
// Input
{
email: string; // format: email, unique
fullName: string; // min: 2, max: 100
roleId?: string; // optional, default: 'user'
},
// Output
{
userId: string; // UUID v4
createdAt: string; // ISO 8601
},
// Error Codes
'EMAIL_CONFLICT' | 'INVALID_ROLE' | 'VALIDATION_FAILED',
// Side Effects
'DB_WRITE:users' | 'EVENT_EMIT:user.created' | 'EXTERNAL_CALL:notify.welcome'
>;
| Kazanç | Açıklama |
|---|---|
| LLM güvenle üretir | Prompt'a CreateUserContract verildiğinde, model hata kodlarını, yan etkileri, şemaları ezberlemek zorunda kalmaz — tipi zaten biliyor. |
| Refactor korkusu biter | Sözleşme değişirse TypeScript derleyici tüm çağrı yerlerini, testleri, mock'ları uyarır. |
| Gözden kaçan yan efekt yok | sideEffects alanında DB_WRITE:users görürsen, "bu fonksiyon transaction içinde mi?" diye sorarsın. |
| SLA visibility | performance.latencyP95Ms: 150 yazıyorsa, load testinde bu sayıyı assert edebilirsin. |
Yeni sözleşme = Tip güvenliği + Şema + Hata kodları + Yan etkiler + SLA
Hepsi derleme zamanında kanıtlanabilir, LLM prompt'una tek bir nesne olarak beslenebilir.
Bir sonraki bölümde bu sözleşmeyi nasıl otomatik testlere, dokümantasyona ve CI kapısına bağlayacağımızı göreceğiz 🚀
Tip Güvenliği ile runtime validation (Zod) arasında pratik bir bağlantı kuracağız. Bu sayede TypeScript'in compile‑time garantilerini korurken, anlaşılabilir bir şema oluşturuyoruz. AI ajanımız böylece "Bu fonksiyon email string alıyor, regex ile kontrol ediliyor ve boş olamaz" diyebilecek.
import { z } from 'zod';
export const UserRegistrationSchema = z.object({
// 📧 Email: string, geçerli format, boş olamaz
email: z
.string({ message: 'Email string olmalı' })
.email({ message: 'Geçerli bir email adresi olmalı' })
.nonempty({ message: 'Email boş olamaz' }),
// 👤 Username: 3‑20 karakter arasında
username: z
.string({ message: 'Kullanıcı adı string olmalı' })
.min(3, { message: 'En az 3 karakter olmalı' })
.max(20, { message: 'En fazla 20 karakter olmalı' }),
// 🔢 Age: isteğe bağlı, pozitif integer
age: z
.number({ message: 'Yaş bir sayı olmalı' })
.int({ message: 'Yaş tam sayı olmalı' })
.positive({ message: 'Yaş pozitif olmalı' })
.optional()
});
/** Zod şemasından türetilen TypeScript tipi */
export type UserRegistrationInput = z.infer<typeof UserRegistrationSchema>;
z.infer tipi, aynı şemanın TypeScript tarafındaki tip temsilcisidir./** UserRegistrationInput tipiyle çalışır, Zod şemasıyla otomatik doğrulanır */
async function registerUser(data: UserRegistrationInput): Promise<void> {
/* 1️⃣ Compile‑time kontrol: TypeScript build sırasında tipi kontrol eder */
/* 2️⃣ Runtime kontrol: Zod her request’te şemayı doğrular */
const parsed = UserRegistrationSchema.parse(data);
// parsed artık garanti edilmiş bir nesnedir 🎉
console.log(`Kayıt: ${parsed.username} (${parsed.email})`);
}
registerUser fonksiyonu, compile‑time garanti (TypeScript) ile runtime güvenlik (Zod) bir arada sunar.email alanı bir string olmalı, regex ile format kontrol ediliyor ve boş olamaz” diyebilir.Bu pratik kombinasyon ile Tip Güvenliğini (type safety) runtime şemasıyla birleştirerek, AI’lar (ve insanlar) işlevlerin beklentilerini kolayca anlayabilir. Artık registerUser fonksiyonu, güvenilir ve self‑dokumented bir API parçası 🎉.
Hazırsan başlayalım. 🛠️ Hadi bir tane consumer-driven contract testine göz atalım ve bunu nasıl "belgeleme" aracına çevirebileceğimizi görelim.
Bu üç kontrol, sağlayıcıya ulaşan isteğin güvenli ve tanımlanabilir olduğunu garanti eder.
Aşağıda, UserProfileProvider sağlayıcısı için iki etkileşim belgeleyen basit bir TypeScript örneği bulunmaktadır:
import { Pact } from '@pactium/pact';
import { Matcher } from '@pactium/pact/src/matchers';
const provider = new Pact({
consumer: 'MyAIConsumer',
provider: 'UserProfileProvider',
});
describe('User Profile API contract', () => {
beforeAll(async () => {
await provider.setup();
});
it('can document happy path', async () => {
await provider.addInteraction({
uponReceiving: 'a request for user profile',
withRequest: {
method: 'GET',
path: '/users/{id}',
headers: { 'Content-Type': 'application/json' },
query: { id: '123' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: '123',
name: 'John Doe',
email: 'john@example.com'
}
}
});
});
it('documents error when user not found', async () => {
await provider.addInteraction({
uponReceiving: 'a request for missing user',
withRequest: {
method: 'GET',
path: '/users/{id}',
headers: { 'Content-Type': 'application/json' },
query: { id: '999' }
},
willRespondWith: {
status: 404,
headers: { 'Content-Type': 'application/json' },
body: {
error: 'Not Found',
message: 'User not found'
}
}
});
});
});
Özet – uponReceiving sağlayıcıya yönelik isteği tanımlar; withRequest isteğin şeklini; willRespondWith ise belgelenen yanıtı içerir (status, header, body). Birkaç test, provider state’lerini açıkça kapsadığından, anlaşma canlı bir belge haline gelir.
docs/ veya contracts/ dizinlerinde saklayın; doğrudan kaynak kodunda değil.pact veya Spring Cloud Contract gibi araçlar, bu sözleşmeleri okunabilir, sürdürülebilir canlı dokümanlara dönüştürerek her birinin kodunuzu tekrar tekrar doğrulamaktan kurtarır.
Hadi gelin "Ben yerelde test ettim, sorun yok" demekten kurtulup, pipeline'ın kendisi bizim yerimize "Agent-Ready" standardını zorlasın 🎯.
Bir contract validation adımı ekleyerek, kodun AI ajanı tarafından güvenle tüketilebilir olduğundan emin oluyoruz.
tsc) — TypeScript'in derleme anında yakaladığı hataları erken yakalıyoruz.Kural basit: Bu adımlardan herhangi biri başarısız olursa merge engellenir ⛔.
Böylecemainbranch'ine giren her commit, AI ajanı için hazır demektir ✅.
.github/workflows/agent-ready.ymlAşağıdaki dosyayı repoya ekleyin. validate-contract job'ı, push ve pull request'te tetiklenir 🔁.
name: Agent-Ready Contract Validation
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
validate-contract:
name: 🔍 Contract & Type Validation
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# 1️⃣ Node.js ortamını hazırla
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# 2️⃣ Bağımlılıkları kur (ci modunda, lockfile'a saygılı)
- name: Install dependencies
run: npm ci
# 3️⃣ Tip kontrolü — tsc --noEmit
- name: Run TypeScript type check
run: npm run typecheck
# package.json'da: "typecheck": "tsc --noEmit"
# 4️⃣ Şema doğrulama testleri
- name: Run schema validation tests
run: npm run test:schema
# Örn: Jest/Vitest ile Zod/JSON Schema testleri
# 5️⃣ Pact contract verification (provider tarafı)
- name: Run Pact verification
run: npm run pact:verify
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
# Gizli bilgileri Settings > Secrets kısmından verin
# 6️⃣ (Opsiyonel) LLM tabanlı statik analiz
# - name: Run LLM static analysis
# run: npx @your-org/llm-code-review@latest
# env:
# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
| Adım | Ne Garanti Ediyor? |
|---|---|
| typecheck | Derleme anında tip hatası yok → runtime sürprizleri azalır |
| test:schema | API girdi/çıktı şemaları tutarlı → AI ajanı "beklenen formatta" veri alır |
| pact:verify | Consumer ile sözleşme bozulmamış → breaking change riski sıfırlanır |
| LLM analizi (ops.) | Kod kalitesi, güvenlik, "agent-friendly" desenler için ek göz |
İpucu:
pact:verifyadımı için Pact Broker URL/token'ını repository secret'ları olarak saklayın.
Böylece hassas bilgiler loglarda görünmez 🔐.
Bu workflow merge'e güvenlik kapısı görevi görür.
Geliştirici "Çalışıyor, merge edin" dese bile, pipeline "Contract bozulmuş, tip hatası var, schema uyumsuz" diyerek engeller ⛔.
Böylece AI ajanı bu kodu çektiğinde:
Hazırsan bir sonraki bölümde bu adımları nasıl yerel geliştirme döngüsüne de entegre edeceğimize bakalım 🔜.
Hazırsan bu bölümü tek bir User Registration akışı üzerinden baştan sona inşa edelim.
Amacımız: bir AI ajanı bu dosyaları okuyup “yeni bir alan ekle” dediğinde sıfır hata ile ne yapacağını bilebilsin 🎯
src/
├─ domain/
│ ├─ user/
│ │ ├─ schema.ts # Zod girdi / çıktı şemaları
│ │ ├─ types.ts # Result / Either hata tipleri
│ │ └─ service.ts # Kayıt iş mantığı
├─ tests/
│ └─ contract.test.ts # Contract (şema) testleri
└─ index.ts # Uygulama giriş noktası (opsiyonel)
Not: Sadece
schema.ts,service.tsvecontract.test.tsdosyalarını tam olarak göstereceğim; diğer dosyalar sadece bağlam için var.
src/domain/user/schema.ts — Girdi & Çıktı şemaları (Zod)// src/domain/user/schema.ts
import { z } from "zod";
/** 1️⃣ Kullanıcı kayıt isteği — AI ajanı bu şemayı okuyup yeni alan eklerken
* sadece `extend` kullanacak, gerisi değişmez. */
export const RegisterInputSchema = z.object({
email: z.string().email({ message: "Geçersiz e‑posta formatı" }),
password: z.string().min(8, { message: "Şifre en az 8 karakter olmalı" }),
// 👇 Yeni alan eklenecekse buraya `z.string().optional()` gibi ekleriz
});
/** 2️⃣ Başarılı kayıt sonrası dönen veri — front‑end / API sözleşmesi */
export const RegisterOutputSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
createdAt: z.string().datetime(),
});
/** Tür çıkarımı — kodda `RegisterInput` / `RegisterOutput` olarak kullanılır */
export type RegisterInput = z.infer<typeof RegisterInputSchema>;
export type RegisterOutput = z.infer<typeof RegisterOutputSchema>;
Ne oldu?
RegisterInputSchema.extend({ ... }) yapar, testler otomatik güncellenir ✅src/domain/user/types.ts — Result / Either hata tipi// src/domain/user/types.ts
/** Basit Result<E, A> — Either pattern'in hafif hali */
export type Result<E, A> =
| { ok: true; value: A }
| { ok: false; error: E };
/** Hata kategorileri — servis katmanı bu union'u döner */
export type RegisterError =
| { type: "ValidationError"; issues: readonly z.ZodIssue[] }
| { type: "EmailTaken" }
| { type: "Unexpected"; cause: unknown };
Neden bu yapı?
ok: true/false sayesinde pattern matching (switch) ile hata yönetimi zorunlu hale gelir ❗️type ekler, derleyici uyarır 🛠src/domain/user/service.ts — Kayıt iş mantığı// src/domain/user/service.ts
import { RegisterInput, RegisterOutput, RegisterInputSchema } from "./schema";
import { Result, RegisterError } from "./types";
import { v4 as uuidv4 } from "uuid";
/** Sahte repo — gerçek projede DB / ORM olur */
const usersDb = new Map<string, RegisterOutput>();
export async function registerUser(
rawInput: unknown
): Promise<Result<RegisterError, RegisterOutput>> {
// 1️⃣ Şema doğrulama — Zod parseAsync hatayı `ZodError` olarak fırlatır
const parseResult = RegisterInputSchema.safeParse(rawInput);
if (!parseResult.success) {
return {
ok: false,
error: { type: "ValidationError", issues: parseResult.error.issues },
};
}
const input: RegisterInput = parseResult.data;
// 2️⃣ İş kuralı: e‑posta benzersiz olmalı
const emailTaken = Array.from(usersDb.values()).some(
(u) => u.email === input.email
);
if (emailTaken) {
return { ok: false, error: { type: "EmailTaken" } };
}
// 3️⃣ Kayıt oluştur
const now = new Date().toISOString();
const newUser: RegisterOutput = {
id: uuidv4(),
email: input.email,
createdAt: now,
};
usersDb.set(newUser.id, newUser);
return { ok: true, value: newUser };
}
Akış özeti
safeParse) → hata ValidationError olarak döner.EmailTaken.RegisterOutput döner.if bloğu yazacak, tip sistemi gerisini garanti altına alır ✅tests/contract.test.ts — Contract (şema) testleri// tests/contract.test.ts
import { describe, it, expect } from "vitest";
import { RegisterInputSchema, RegisterOutputSchema } from "../src/domain/user/schema";
describe("📄 User Registration Contract", () => {
it("geçerli girdi şemasını karşılar", () => {
const valid = { email: "test@example.com", password: "secret123" };
const result = RegisterInputSchema.safeParse(valid);
expect(result.success).toBe(true);
});
it("geçersiz e‑posta reddedilir", () => {
const invalid = { email: "not-an-email", password: "secret123" };
const result = RegisterInputSchema.safeParse(invalid);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toContain("Geçersiz e‑posta");
}
});
it("çıktı şeması beklenen alanları içerir", () => {
const sampleOutput = {
id: "c0ffee00-dead-beef-cafe-123456789abc",
email: "test@example.com",
createdAt: "2024-01-01T00:00:00.000Z",
};
const result = RegisterOutputSchema.safeParse(sampleOutput);
expect(result.success).toBe(true);
});
});
Neden contract test?
RegisterInputSchema.extend({ phone: z.string().optional() }) yazdığında testleri güncellemek zorunlu hale gelir, aksi takdirde build fails ⛔Bak şimdi — yeni bir
phonealanı eklemek istiyorsun.
schema.ts→RegisterInputSchema.extend({ phone: z.string().optional() })service.ts→input.phonevarsa kaydet (veya ayrı tablo).contract.test.ts→phoneiçeren geçerli/geçersiz case'ler ekle.types.ts→ hata tipleri değişmediyse dokunma.
Hepsi bu kadarı. Derleyici ve testler seni yönlendirir, sen sadece iş mantığına odaklan 🎉
Hazırsan kültür konuşalım 🎯. Kod yazarken sözleşme (contract) düşünmek, sadece test yazmaktan çok daha güçlü bir alışkanlıktır.
Code Review Checklist'ine şu soruyu ekle:
Bu değişiklik sözleşmeyi bozar mı?
Contract First geliştirme alışkanlığı kur:
Terminolojiye Tip Güvenliği ve Agent-Ready Code kavramlarını yerleştir.
Yeni başlayanlara (ve AI ajanlarına) “Sözleşme önce, kod sonra” mantığını anlat.
## Sözleşme Değişiklik Kontrolü
- [ ] Girdi/Çıktı şemaları güncellendi
- [ ] Contract testleri geçiyor
- [ ] Breaking change varsa migration planı eklendi
Bu sayede herkes “Ne değişti? Ne bozuldu? Nasıl migrate edeceğiz?” sorularını PR açıklamasından anlar.
Bu alışkanlık size yarın gelecek AI mesai arkadaşınız için en iyi onboarding belgesini yazmış olmanızı sağlar 🚀. Hadi bugünden başlayalım! ✨
Hazırsan, özetleyelim ve harekete geçelim 🚀
Ana noktalar hızlıca:
Sana soru:
Hangi projenizde bugün bir “Agent‑Ready” test yazmaya başlayacaksınız? 🤔
Küçük bir adım at, bir test yaz, tip tanımını netleştir, pipeline’a ekle. Böylece kod tabanın hem insan hem de geleceğin AI ortakları için anlaşılır, güvenilir ve genişletilebilir hale gelecek ✨
Hadi, ilk adımı birlikte atalım — gelecek senin kodunda başlıyor! 🌟
Bu içerik tamamen yapay zeka destekli otomasyon sistemi ile üretilmiştir.
All rights reserved