---
title: 'Test Piramidi'
source: 'https://academia.sh/tr/kurslar/tumlesim-testi/test-piramidi'
course: 'Tümleşim, Sözleşme ve Uçtan Uca Test'
language: tr
updated: '2026-08-17T18:10:51+00:00'
license: 'CC BY-SA 4.0'
---

# Test Piramidi

Aynı ödünç akışının birim, tümleşim ve uçtan uca seviyelerinde koşturulması; her seviyenin yakaladığı ve kaçırdığı kusur sınıfının gösterilmesi ve seviye başına bedelin sorgu, istek ve kurulum sayısıyla ölçülmesi.

Önceki konu dört denetim kurdu: isteğin ve yanıtın şemaya uyduğu, tüketicinin beklentisinin
sağlayıcıda koştuğu, bir şema değişikliğinin kırıcı olup olmadığı ve sahte sunucunun
sözleşmeye bağlı kaldığı sınandı. Hepsinin ortak yanı şudur: her biri **tek bir sınıra**
bakar. Üyenin bir kitabı ödünç alması ise o sınırların hiçbirine sığmaz — kural bir yerde,
kayıt başka bir yerde, arayüzün gördüğü yanıt üçüncüsündedir.

Seviyelerin sınıflandırması Kalite ve Test Temelleri kursunda yapıldı; burada sorulan şey
sınıflandırma değil **dağılımdır**. Kaç test hangi seviyede olmalı sorusunun yanıtı bir
tercih değildir: seviye başına koşum bedeli, yakalanan kusur sınıfı ve aynı kusuru birden
çok seviyede yakalamanın maliyeti ölçülür, dağılım bu üç sayıdan çıkar.

## Aynı Akış, Üç Seviye

Ödünç alma akışı üç parçaya ayrılmış durumda: uygunluk kuralı saf bir işlev, kayıt gerçek
bir veritabanı, dış yüzey bir HTTP arayüzü. Kural, açık ödünç sayısına bir sınır koyar.

```js
// kural.mjs — surum 1: odunc uygunlugu kurali
export const SINIR = 5;

export function uygunMu({ acikOdunc, gecikmeGunu, uyelik, kitapDurumu }) {
  if (uyelik !== 'etkin') return { uygun: false, sebep: 'uyelik-etkin-degil' };
  if (kitapDurumu !== 'rafta') return { uygun: false, sebep: 'kitap-rafta-degil' };
  if (gecikmeGunu > 0) return { uygun: false, sebep: 'gecikmis-odunc-var' };
  if (acikOdunc > SINIR) return { uygun: false, sebep: 'odunc-siniri-asildi' };
  return { uygun: true, sebep: 'uygun' };
}
```

Depo katmanı kararı gerçek satırlara çevirir. Her sorgu sayılır; bu sayaç dersin bedel
ölçüsüdür.

```js
// depo.mjs — katalog ve odunc kayitlari node:sqlite uzerinde
import { DatabaseSync } from 'node:sqlite';
import { uygunMu } from './kural.mjs';

export const sayac = { sorgu: 0 };

export function depoKur() {
  const db = new DatabaseSync(':memory:');
  db.exec(`CREATE TABLE uye (uye_no TEXT PRIMARY KEY, uyelik TEXT, gecikme_gunu INTEGER);
           CREATE TABLE kitap (kitap_no TEXT PRIMARY KEY, durum TEXT);
           CREATE TABLE odunc (uye_no TEXT, kitap_no TEXT UNIQUE)`);
  return db;
}

export function oduncAl(db, uyeNo, kitapNo) {
  const oku = (s, ...p) => { sayac.sorgu += 1; return db.prepare(s).all(...p); };
  const yaz = (s, ...p) => { sayac.sorgu += 1; db.prepare(s).run(...p); };
  const [uye] = oku('SELECT uyelik, gecikme_gunu FROM uye WHERE uye_no = ?', uyeNo);
  const [kitap] = oku('SELECT durum FROM kitap WHERE kitap_no = ?', kitapNo);
  const [{ n }] = oku('SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?', uyeNo);
  const karar = uygunMu({
    acikOdunc: n, gecikmeGunu: uye.gecikme_gunu, uyelik: uye.uyelik, kitapDurumu: kitap.durum,
  });
  if (karar.uygun === false) return karar;
  yaz('INSERT INTO odunc (uye_no, kitap_no) VALUES (?, ?)', uyeNo, kitapNo);
  yaz("UPDATE kitap SET durum = 'oduncte' WHERE kitap_no = ?", kitapNo);
  return karar;
}
```

Dış yüzeyde iki parça var: kararı bir durum koduna eşleyen küçük işlev ve isteği karşılayan
sunucu.

```js
// durumkodu.mjs — surum 1: karar HTTP durum koduna eslenir
export const durumKodu = (karar) => (karar.uygun ? 201 : 409);
```

```js
// servis.mjs — odunc akisinin HTTP arayuzu
import { createServer } from 'node:http';
import { oduncAl } from './depo.mjs';
import { durumKodu } from './durumkodu.mjs';

export function sunucuKur(db) {
  return createServer((istek, yanit) => {
    let govde = '';
    istek.on('data', (parca) => { govde += parca; });
    istek.on('end', () => {
      const { uyeNo, kitapNo } = JSON.parse(govde);
      const karar = oduncAl(db, uyeNo, kitapNo);
      yanit.writeHead(durumKodu(karar), { 'content-type': 'application/json' });
      yanit.end(JSON.stringify(karar));
    });
  });
}
```

Dört test aynı akışı üç seviyede sınar: biri yalnız kuralı, biri kuralı ve veritabanını,
ikisi de kullanıcının gördüğü yolu.

```js
// seviyeler.test.mjs — ayni odunc akisi uc seviyede sinanir
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { uygunMu } from './kural.mjs';
import { depoKur, oduncAl } from './depo.mjs';
import { sunucuKur } from './servis.mjs';

const doldur = () => {
  const db = depoKur();
  db.prepare("INSERT INTO uye VALUES ('U-17', 'etkin', 0)").run();
  db.prepare("INSERT INTO uye VALUES ('U-31', 'askida', 0)").run();
  db.prepare("INSERT INTO kitap VALUES ('K-90', 'rafta')").run();
  return db;
};

const gonder = async (govde) => {
  const sunucu = sunucuKur(doldur());
  await new Promise((bitir) => sunucu.listen(0, bitir));
  const yanit = await fetch(`http://127.0.0.1:${sunucu.address().port}/odunc`,
    { method: 'POST', body: JSON.stringify(govde) });
  const icerik = await yanit.json();
  sunucu.close();
  return { durum: yanit.status, sebep: icerik.sebep };
};

test('birim: besinci acik odunc siniri doldurur', () => {
  const taban = { gecikmeGunu: 0, uyelik: 'etkin', kitapDurumu: 'rafta' };

  assert.equal(uygunMu({ ...taban, acikOdunc: 4 }).uygun, true);
  assert.equal(uygunMu({ ...taban, acikOdunc: 5 }).sebep, 'odunc-siniri-asildi');
});

test('tumlesim: odunc satiri yazilir ve kitabin durumu doner', () => {
  const db = doldur();

  assert.equal(oduncAl(db, 'U-17', 'K-90').uygun, true);
  const satir = db.prepare('SELECT uye_no, kitap_no FROM odunc').get();
  assert.equal(`${satir.uye_no} ${satir.kitap_no}`, 'U-17 K-90');
  assert.equal(db.prepare("SELECT durum FROM kitap WHERE kitap_no = 'K-90'").get().durum, 'oduncte');
});

test('uctan uca: uygun istek onay dondurur', async () => {
  assert.deepEqual(await gonder({ uyeNo: 'U-17', kitapNo: 'K-90' }), { durum: 201, sebep: 'uygun' });
});

test('uctan uca: askidaki uyelik reddedilir', async () => {
  assert.deepEqual(await gonder({ uyeNo: 'U-31', kitapNo: 'K-90' }),
    { durum: 409, sebep: 'uyelik-etkin-degil' });
});
```

## Sınır Karşılaştırması Kusuru

Kuralın birinci sürümünde sınır karşılaştırması yanlış yazılmış: sınır beş açık ödünçse,
beşi olan üye altıncıyı alamamalıdır. `acikOdunc > SINIR` yazımı altıncıyı verir ve ancak
yedincide durur.

```bash
node --test --test-reporter=tap seviyeler.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 3
# fail 1
```

Birim seviyesi kusuru yakaladı. Aynı koşumda dikkat çeken şey diğer üç satırdır: kusur
koddayken tümleşim ve uçtan uca testleri **yeşil kaldı**. Sebebi basit bir seçimdir — o iki
test mutlu yolu koşuyor, yani sıfır açık ödüncü olan bir üyeyi. Sınır kusurunu üst
seviyede yakalamak için üst seviyede de beş açık ödünçlü bir üye hazırlanmalıydı; bunun
bedeli bir sonraki bölümün konusudur.

Düzeltme tek karşılaştırmadadır.

```js
// kural.mjs — surum 2: sinir karsilastirmasi duzeltildi
export const SINIR = 5;

export function uygunMu({ acikOdunc, gecikmeGunu, uyelik, kitapDurumu }) {
  if (uyelik !== 'etkin') return { uygun: false, sebep: 'uyelik-etkin-degil' };
  if (kitapDurumu !== 'rafta') return { uygun: false, sebep: 'kitap-rafta-degil' };
  if (gecikmeGunu > 0) return { uygun: false, sebep: 'gecikmis-odunc-var' };
  if (acikOdunc >= SINIR) return { uygun: false, sebep: 'odunc-siniri-asildi' };
  return { uygun: true, sebep: 'uygun' };
}
```

```bash
node --test --test-reporter=tap seviyeler.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 4
# fail 0
```

## Yalnız Üst Seviyenin Gördüğü Kusur

Ters yönde bir kusur sınıfı da vardır: alt seviyede karşılığı olmayan bir kusur. Reddedilen
bir ödünç isteğinin durum kodu bunlardan biridir — kural için "uygun değil" tek bir değerdir,
depo için kayıt yazmamaktır, ama arayüzü çağıran istemci için bu değer bir durum kodudur.

```js
// durumkodu.mjs — surum 2: red yaniti basari kodu ile donuyor
export const durumKodu = (karar) => (karar.uygun ? 201 : 200);
```

```bash
node --test --test-reporter=tap seviyeler.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
not ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 3
# fail 1
```

Kural ve depo testleri yeşil; kusur onların gördüğü hiçbir değerde değil. Düzeltme, eşlemeyi
geri almaktır.

```js
// durumkodu.mjs — surum 3: red yaniti catisma kodu ile doner
export const durumKodu = (karar) => (karar.uygun ? 201 : 409);
```

```bash
node --test --test-reporter=tap seviyeler.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 4
# fail 0
```

Eşlemenin kendi işlevine çıkarılmış olması burada rastlantı değildir: `durumKodu` ayrı bir
işlev olduğu için ona bir birim testi yazılabilir ve o kusur bir daha uçtan uca seviyesine
bırakılmaz. Piramit bir sayım kuralı olmadan önce bir tasarım baskısıdır — aşağı
indirilebilen denetim aşağı indirilir.

## Bedel

Kural dört koşula bakıyor: üyelik, kitap durumu, gecikme ve açık ödünç sayısı. Sınır
davranışını görmek için açık ödünç değerinin üç değeri gerekir (dördü, beşi, altısı); diğer
üç koşulun ikişer değeri vardır. Toplam yirmi dört vaka. Aşağıdaki ölçüm aynı yirmi dört
vakayı üç seviyede koşturur ve seviye başına bedeli sayar.

```js
// bedel.mjs — ayni vaka kumesini uc seviyede kosmanin bedeli sayilir
import { uygunMu } from './kural.mjs';
import { depoKur, oduncAl, sayac } from './depo.mjs';
import { sunucuKur } from './servis.mjs';

const vakalar = [];
for (const uyelik of ['etkin', 'askida']) {
  for (const kitapDurumu of ['rafta', 'oduncte']) {
    for (const gecikmeGunu of [0, 3]) {
      for (const acikOdunc of [4, 5, 6]) vakalar.push({ uyelik, kitapDurumu, gecikmeGunu, acikOdunc });
    }
  }
}

const olcum = { kurulum: 0, istek: 0 };
const hazirla = (vaka) => {
  const db = depoKur();
  db.prepare('INSERT INTO uye VALUES (?, ?, ?)').run('U-17', vaka.uyelik, vaka.gecikmeGunu);
  db.prepare('INSERT INTO kitap VALUES (?, ?)').run('K-90', vaka.kitapDurumu);
  olcum.kurulum += 2;
  for (let i = 0; i < vaka.acikOdunc; i += 1) {
    db.prepare('INSERT INTO odunc VALUES (?, ?)').run('U-17', `E-${i}`);
    olcum.kurulum += 1;
  }
  return db;
};

const s = (n, g) => String(n).padStart(g);
const olc = async (ad, bilesen, kosu) => {
  olcum.kurulum = 0;
  olcum.istek = 0;
  sayac.sorgu = 0;
  const basla = performance.now();
  await kosu();
  const sure = performance.now() - basla;
  console.log(`${ad.padEnd(10)}${s(bilesen, 7)}${s(vakalar.length, 6)}${s(olcum.kurulum, 9)}`
    + `${s(sayac.sorgu, 7)}${s(olcum.istek, 7)}`);
  return sure;
};

console.log(`${'seviye'.padEnd(10)}${s('bilesen', 7)}${s('vaka', 6)}${s('kurulum', 9)}${s('sorgu', 7)}${s('istek', 7)}`);

const birim = await olc('birim', 1, () => {
  for (const vaka of vakalar) uygunMu(vaka);
});

const tumlesim = await olc('tumlesim', 2, () => {
  for (const vaka of vakalar) oduncAl(hazirla(vaka), 'U-17', 'K-90');
});

const uctanUca = await olc('uctan uca', 3, async () => {
  for (const vaka of vakalar) {
    const sunucu = sunucuKur(hazirla(vaka));
    await new Promise((bitir) => sunucu.listen(0, bitir));
    await fetch(`http://127.0.0.1:${sunucu.address().port}/odunc`,
      { method: 'POST', body: JSON.stringify({ uyeNo: 'U-17', kitapNo: 'K-90' }) });
    olcum.istek += 1;
    sunucu.close();
  }
});

const kat = (a, b, esik) => (a / b > esik ? 'evet' : 'hayir');
console.log(`tumlesim suresi birimin 10 katindan uzun mu: ${kat(tumlesim, birim, 10)}`);
console.log(`uctan uca suresi birimin 100 katindan uzun mu: ${kat(uctanUca, birim, 100)}`);
```

```
seviye    bilesen  vaka  kurulum  sorgu  istek
birim           1    24        0      0      0
tumlesim        2    24      168     74      0
uctan uca       3    24      168     74     24
tumlesim suresi birimin 10 katindan uzun mu: evet
uctan uca suresi birimin 100 katindan uzun mu: evet
```

Süre ölçümü makineye ve yüke bağlıdır; bu yüzden çıktı ham milisaniye değil, oran eşiği
basar. Bu koşumda yirmi dört vaka birim seviyesinde 0,02 ms, tümleşim seviyesinde 1,8 ms,
uçtan uca seviyesinde 27,8 ms sürdü; başka bir makinede üç sayı da değişir, sıralamaları
değişmez. Koşumdan bağımsız nicelikler tablodadır: aynı yirmi dört vaka birim seviyesinde
hiçbir sorgu ve istek üretmezken, üst iki seviyede 168 kurulum satırı ve 74 akış sorgusu
üretiyor, en üstte bunlara 24 ağ isteği ekleniyor. Bileşen sütunu da bir bedeldir: bir
seviye kaç parçanın aynı anda ayakta olmasını gerektiriyorsa, o kadar arıza kaynağı
taşır.

Aynı iddianın üç seviyede yinelenmesi bu bedelleri toplar. Yirmi dört vaka her seviyede
tekrarlansa yetmiş iki test olur; kural bir gün değiştiğinde üç ayrı dosya güncellenir ve
üç koşum yeniden yeşile getirilir. Yinelemenin karşılığında yeni bir kusur sınıfı
yakalanmaz: üst seviyeler aynı kararı, üstüne bir taşıma katmanı bindirerek okur.

Piramidin biçimi bu iki gözlemin toplamıdır. Alt seviyede bir vaka bedava sayılır, bu
yüzden vaka kümesi orada tüketilir. Üst seviyede her vaka kurulum, sorgu ve istek satın
alır; bu yüzden orada yalnızca **alt seviyede karşılığı olmayan** kusur sınıfları sınanır —
parçaların birbirine bağlanması, taşıma katmanının eşlemesi, kullanıcının gördüğü yolun
bütünlüğü. Tersi biçim, yani vaka kümesinin üstte tüketilmesi, aynı kusurları çok daha
pahalıya ve çok daha yavaş yakalar.

## Özet

- Ödünç akışı üç seviyede koşturuldu: kural saf fonksiyon olarak, kayıt gerçek veritabanıyla,
  dış yüzey HTTP arayüzüyle.
- Sınır karşılaştırması kusurunu yalnız birim seviyesi yakaladı; üst iki seviye mutlu yolu
  koştuğu için yeşil kaldı.
- Durum kodu eşlemesi kusurunu yalnız uçtan uca seviyesi yakaladı; kural ve depo testleri
  o değeri hiç görmüyor.
- Yirmi dört vakanın bedeli seviyeye göre 0 sorgu ve 0 istekten 242 sorgu ve 24 isteğe
  çıktı; süre oranı bu koşumda birim seviyesinin yüz katını aştı.
- Piramit bir sayım kuralı değil, iki ölçünün sonucudur: vaka kümesi bedeli düşük seviyede
  tüketilir, üst seviyeye yalnız aşağıda karşılığı olmayan kusur sınıfları kalır.

## Sonraki Adım

Buradaki uçtan uca testler HTTP arayüzünü sürdü: bir istek gönderildi, bir durum kodu
okundu. Üyenin gerçekte gördüğü yüzey ise bir sayfadır — üzerinde bir arama kutusu, bir
kitap kartı ve bir düğme vardır. O yüzeyi süren bir test, hangi ögeye dokunacağını bir
**seçiciyle** söylemek ve ögenin hazır olmasını beklemek zorundadır. Sonraki ders bu iki
kararı ölçer: seçici türü değiştiğinde testin kaç yapı değişikliğine dayandığını ve bekleme
politikasının düşme oranını nasıl değiştirdiğini.
