---
title: 'Birim Test Çerçevesi'
source: 'https://academia.sh/tr/kurslar/java-ekosistem/birim-test-cercevesi'
course: 'Derleme, Test ve Ekosistem'
language: tr
updated: '2026-08-23T14:25:07+00:00'
license: 'CC BY-SA 4.0'
---

# Birim Test Çerçevesi

Kurulum, gövde ve sökme çağrılarını sırayla yapan küçük bir koşturucu elle kurulur; aynı iddia sökmeden önce ve sonra kontrol edildiğinde iki ayrı sonuç veriyor, ikisi de koştu sayılıyor. Aynı derlenmiş test sınıfı, çerçeve tipi eksik bir sınıf yoluyla çalıştırılınca test sınıf yolunun üçüncü, bağımsız bir yol olduğu ölçülüyor.

Önceki konu koşum sınıf yolunu taşınabilir bir ürüne çevirdi ve içine ne konduğunun bir karar
olduğunu gösterdi. O karar orada iki yolla sınırlıydı: derleme sınıf yolu ve koşum sınıf yolu.
Bu ders aynı kararı üçüncü bir yol için verir. Test yazarken elimizin altında bir sınıf yolu
daha durur — testin gördüğü yol — ve bu yol üretimin gördüğü yolla aynı değildir. Sorumuz şu:
bu üçüncü yol neyi taşır, ve üretim sınıf yolunda hiç bulunmayan bir tip eksik olduğunda ne
olur?

Sorunun ikinci yarısı ayrı bir konudur ve M21/K02 Birim Testi ve Test Odaklı Geliştirme
kursunda kuruldu: test sınıflandırması, iddia yazımı, test adlandırma — bunların hiçbiri burada
yeniden kurulmaz. Bu derste tek yeni soru, testin çalışması için gereken tiplerin hangi sınıf
yolunda durduğudur. O soruyu yanıtlamadan önce, üzerine bir sınıf yolu sorusu kuracağımız
çerçevenin kendisini — kurulum, gövde ve sökme çağrılarını sırayla yapan koşturucuyu — elimizle
kurmamız gerekiyor.

Bu iki soru göründüğünden daha yakın duruyor. Bir çerçeve, kaynakta hiç yazılmayan bir sırayı
her olgu için tekrar tekrar uygular; bu tekrar, bir dosyadaki kaynağın hiçbir yerinde
görünmeyen bir dizi çağrı anlamına gelir. O çağrıların derlenip koşabilmesi için bir yere
yazılmış olması gerekir — ve o yer, üretim kodunun okuduğu sınıf yolu değildir. Çerçeveyi
kurmadan sınıf yolu sorusuna geçmek, boş bir soyutlamayı ölçmek olurdu; bu yüzden ders önce
çağrıları elle yazıyor, sonra o çağrıların hangi tipe ihtiyaç duyduğunu sayıyor.

## Çerçevenin Kurduğu Sıra

- **TS1.** Kurulum ve sökme burada birer `Runnable` olarak modellenir; gerçek bir çerçevenin
  test keşfi (dosya tarama, ad eşleme) burada kurulmaz — yalnız kurulum, gövde, sökme sırası ve
  başarısızlığın bildirilme biçimi modellenir.
- **TS2.** İddia başarısızlığı bir `AssertionError` fırlatarak bildirilir; koşturucu bunu
  yakalar ve olguyu "düştü" diye günlükler, aksi hâlde "geçti" yazar. Sökme adımı her koşulda —
  gövde düşse de düşmese de — çalışır.

Bir test çerçevesinin yaptığı iş, dışarıdan bakınca üç çağrıyı belirli bir sırayla yapmaktan
ibarettir: önce kurulum, sonra gövde, en sonda sökme. İddia bir karşılaştırmadır ve
başarısızlığını bir istisnayla bildirir; çerçeve bu istisnayı yakalayıp olguyu "düştü" diye
işaretler, yakalamazsa "geçti" yazar. Kurulum her olgu için yeniden çalışır — bir olgunun
bıraktığı durum bir sonrakine sızmaz — ve sökme, olgu geçse de düşse de çalışır; aksi hâlde
bir olgunun düşmesi bir sonrakinin kurulumunu bozardı. Aşağıdaki koşturucu bu üç çağrıyı ve
iddiayı elle kurar.

```java
// Cerceve.java — kurulum, govde ve sokme cagrilarini sirayla yapan kucuk bir kosturucu
public class Cerceve {

    static void olguKosustur(String ad, Runnable kurulum, Runnable govde, Runnable sokme) {
        kurulum.run();
        try {
            govde.run();
            System.out.println(pad(ad) + "gecti");
        } catch (AssertionError hata) {
            System.out.println(pad(ad) + "dustu: " + hata.getMessage());
        } finally {
            sokme.run();
        }
    }

    static void iddiaEsit(String alan, int beklenen, int gelen) {
        if (beklenen != gelen) throw new AssertionError(alan + " (beklenen " + beklenen + ", gelen " + gelen + ")");
    }

    static String pad(String s) { return String.format("%-16s", s); }

    public static void main(String[] args) {
        int[] miktar = {0};
        olguKosustur("depo-dolu",
                () -> miktar[0] = 10,
                () -> iddiaEsit("miktar", 10, miktar[0]),
                () -> miktar[0] = 0);

        int[] alan = {0};
        Runnable kurulum = () -> alan[0] = 5;
        Runnable sokme = () -> alan[0] = 0;

        olguKosustur("once-bakan", kurulum, () -> iddiaEsit("alan", 5, alan[0]), sokme);

        kurulum.run();
        olguKosustur("sonra-bakan", () -> {}, () -> { sokme.run(); iddiaEsit("alan", 5, alan[0]); }, () -> {});
    }
}
```

```
depo-dolu       gecti
once-bakan      gecti
sonra-bakan     dustu: alan (beklenen 5, gelen 0)
```

İlk olgu sıradan bir geçiş: kurulum `miktar`'ı 10 yapıyor, gövde bunu okuyup doğruluyor, sökme
sıfırlıyor. Asıl soru ikinci ve üçüncü olguda. Her ikisi de aynı iddiayı taşıyor — `alan`'ın
değeri 5 olmalı — ama biri iddiayı sökmeden **önce**, öteki sökmeden **sonra** kontrol ediyor.
Sökme `alan`'ı sıfırlıyor; bu yüzden aynı iddia birinci olguda geçiyor, ikincisinde düşüyor.
Koşturucunun kendisi bu ayrımı bilmiyor — o yalnız üç çağrıyı sırayla yapıyor ve gövdenin
fırlattığı istisnayı yakalıyor. `once-bakan` ve `sonra-bakan` çıktıda ikisi de birer satır
olarak görünüyor, ikisi de koşturucunun listesinde "koştu" sayılıyor; aralarındaki fark yalnız
sonuç sütununda okunuyor.

Buradan çıkan sonuç şudur: çerçevenin çağırdığı sıra bir garanti değil, bir sözleşmedir.
Kurulum-gövde-sökme sırası her zaman aynı kalır, ama gövdenin **ne zaman** okuduğu — kurulumun
bıraktığı durumu mu, sökmenin bıraktığı durumu mu — yazan kişinin elindedir. Aynı iddia metni,
iki farklı konumda yazıldığında iki farklı gerçeği ölçer, ve çerçeve ikisi arasında hakemlik
yapmaz; yalnız hangisinin fırlattığını, hangisinin fırlatmadığını kaydeder. Bir olgunun "koştu"
sayılması, o olgunun **doğru** kurulduğu anlamına gelmez — yalnız üç çağrının sırayla yapıldığı
ve bir sonucun üretildiği anlamına gelir. M21/K02'nin kurduğu test sınıflandırması ve iddia
yazımı burada değişmiyor; değişen tek şey, iddianın gövde içinde nereye yazıldığının sonucu
belirlediğidir.

Bu ayrımın kaynağı `finally` bloğudur: sökme çağrısı gövdenin sonucundan bağımsız olarak
çalışır, çünkü koşturucu onu `try` bloğunun dışına değil, `finally` içine koymuştur. Sökme
adımı bu yüzden hem `depo-dolu` gibi geçen bir olguda hem `sonra-bakan` gibi düşen bir olguda
aynı şekilde işler — `alan`'ı sıfırlar. Fark, iddianın bu sıfırlamadan **önce mi sonra mı**
çalıştığındadır, ve bu bilgi koşturucunun kendisinde değil, gövdeyi yazan kişinin
kararındadır. Gerçek bir çerçevede de durum aynıdır: kurulum ve sökmenin çağrılma sırası
belgelenir ve garanti edilir, ama gövdenin o sırayı nasıl kullandığı — hangi anı, hangi
durumu okuduğu — çerçevenin denetiminde değildir.

## Test Sınıf Yolu: Üçüncü Bir Yol

- **TS3.** Derleme sınıf yolu ve koşum sınıf yolu ayrımı önceki konudan (java-build)
  devralınır ve burada yeniden kurulmaz; bu derste eklenen tek soru, üçüncü bir sınıf yolunun
  neyi taşıdığıdır.
- **TS4.** "Tip sayısı" burada derlenmiş `.class` dosyalarının sayımıdır. Aşağıdaki üç kaynağın
  her biri iç sınıf içermediği için tam olarak bir `.class` dosyası üretir; sayım bu yüzden
  kaynak sayısıyla örtüşüyor, ama genel olarak tip sayısı kaynak dosyası sayısından farklı
  çıkabilir.
- **TS5.** "Çerçeve tipi olmadan" koşum, gerçek bir sınıf yükleyicisinin, verilen sınıf yolunda
  o tipi arayıp bulamamasıdır — yükleyici burada `URLClassLoader`'dır ve platform sınıf
  yükleyicisinin üstüne kurulur, böylece uygulamanın kendi sınıf yolundaki hiçbir şeyi miras
  almaz.

Yukarıdaki koşturucu ve içindeki iddia yardımcısı `Cerceve` sınıfının bir parçasıydı — ama
gerçek bir programda bu tür bir yardımcı, sınanan koddan **ayrı** bir yerde durur. Aşağıdaki
ölçüm üç ayrı sınıfı gerçekten derliyor: `Depo` (üretim kodu), `Iddia` (çerçevenin kendisi,
yalnız testin ihtiyaç duyduğu bir eşitlik kontrolü) ve `DepoTest` (ikisini de kullanan test).
`Depo` yalnız kendi sınıf yolunda derleniyor; `Iddia` ayrı bir dizinde derleniyor; `DepoTest`
ikisine karşı derleniyor. Sonra iki ayrı sınıf yoluyla koşturuluyor: biri üçünü de içeriyor,
öteki `Iddia`'yı dışarıda bırakıyor.

```java
// UcuncuYol.java — test sinif yolunun uretim sinif yolundan fazladan tasidigi tipler gercekten sayilir
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class UcuncuYol {

    static Path yaz(Path dizin, String ad, String kaynak) throws IOException {
        Files.createDirectories(dizin);
        Path p = dizin.resolve(ad + ".java");
        Files.writeString(p, kaynak);
        return p;
    }

    static boolean derle(Path cikti, List<Path> sinifYolu, List<Path> kaynaklar) throws IOException {
        Files.createDirectories(cikti);
        List<String> arg = new ArrayList<>(List.of("-d", cikti.toString()));
        if (!sinifYolu.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : sinifYolu) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path k : kaynaklar) arg.add(k.toString());
        ToolProvider derleyici = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter sessizCikti = new StringWriter();
        return derleyici.run(new PrintWriter(sessizCikti), new PrintWriter(sessizCikti),
                arg.toArray(new String[0])) == 0;
    }

    static String kos(List<Path> sinifYolu, String anaSinif) {
        URL[] url = new URL[sinifYolu.size()];
        try {
            for (int i = 0; i < url.length; i++) url[i] = sinifYolu.get(i).toUri().toURL();
            try (URLClassLoader yukleyici = new URLClassLoader(url, UcuncuYol.class.getClassLoader().getPlatformClassLoader())) {
                Class<?> sinif = Class.forName(anaSinif, true, yukleyici);
                sinif.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
                return "tamam";
            }
        } catch (InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }

    static long tipSayisi(Path dizin) throws IOException {
        try (var akis = Files.walk(dizin)) {
            return akis.filter(p -> p.toString().endsWith(".class")).count();
        }
    }

    public static void main(String[] args) throws Exception {
        Path kok = Files.createTempDirectory("ucuncuyol");
        Path kaynak = kok.resolve("kaynak");
        Path uretimD = kok.resolve("uretim"), cercD = kok.resolve("cerceve"), testD = kok.resolve("test");

        Path sDepo = yaz(kaynak, "Depo",
                "public class Depo {\n"
              + "  private int miktar;\n"
              + "  public Depo(int b) { miktar = b; }\n"
              + "  public void ekle(int adet) { miktar += adet; }\n"
              + "  public void azalt(int adet) {\n"
              + "    if (adet > miktar) throw new IllegalStateException(\"yetersiz miktar\");\n"
              + "    miktar -= adet;\n"
              + "  }\n"
              + "  public int miktar() { return miktar; }\n"
              + "}\n");
        Path sIddia = yaz(kaynak, "Iddia",
                "public class Iddia {\n"
              + "  public static void esit(int beklenen, int gelen) {\n"
              + "    if (beklenen != gelen) throw new AssertionError(\"beklenen \" + beklenen + \", gelen \" + gelen);\n"
              + "  }\n"
              + "}\n");
        Path sDepoTest = yaz(kaynak, "DepoTest",
                "public class DepoTest {\n"
              + "  public static void main(String[] a) {\n"
              + "    Depo d = new Depo(10);\n"
              + "    d.ekle(5);\n"
              + "    d.azalt(3);\n"
              + "    Iddia.esit(12, d.miktar());\n"
              + "    System.out.println(\"olgu gecti\");\n"
              + "  }\n"
              + "}\n");

        derle(uretimD, List.of(), List.of(sDepo));
        derle(cercD, List.of(), List.of(sIddia));
        derle(testD, List.of(uretimD, cercD), List.of(sDepoTest));

        System.out.println("uretim sinif yolundaki tip sayisi : " + tipSayisi(uretimD));
        System.out.println("test sinif yolundaki tip sayisi   : "
                + (tipSayisi(uretimD) + tipSayisi(cercD) + tipSayisi(testD)));

        System.out.println("tam test sinif yoluyla kosum        : " + kos(List.of(uretimD, cercD, testD), "DepoTest"));
        System.out.println("cerceve tipi olmadan (uretim+test)  : " + kos(List.of(uretimD, testD), "DepoTest"));
    }
}
```

```
uretim sinif yolundaki tip sayisi : 1
test sinif yolundaki tip sayisi   : 3
olgu gecti
tam test sinif yoluyla kosum        : tamam
cerceve tipi olmadan (uretim+test)  : NoClassDefFoundError
```

Sayılar iddiayı doğruluyor. `Depo` tek başına derlenip koştuğunda üretim sınıf yolu tek bir
tip taşıyor. `DepoTest`'i derlemek ve koşturmak için gereken tam küme ise üç tip: üretim kodu,
çerçevenin kendisi ve testin kendisi. Bu fazladan iki tip — `Iddia` ve `DepoTest` — üretim
programının hiçbir yerinde geçmiyor; dağıtılan çalıştırılabilir çıktıya konmaları gerekmiyor,
konursa gereksiz büyür. Test sınıf yolu bu yüzden üretim sınıf yolunun bir üst kümesi değil,
**ayrı** bir yoldur: üretimin ihtiyaç duymadığı tipleri taşır ve üretimin taşıdığı tipleri de
kapsar.

Yön tek taraflıdır. `DepoTest`'i derlemek `Depo`'yu ve `Iddia`'yı sınıf yoluna koymayı
gerektiriyor, çünkü test ikisini de kaynağında adıyla anıyor. `Depo`'yu derlemek ise ne
`Iddia`'ya ne `DepoTest`'e ihtiyaç duyuyor — üretim kodu test tiplerinin varlığından habersiz
yazıldı. Paketleme konusunun kurduğu ayrım burada aynen geçerli: bir çalıştırılabilir çıktı
üretim sınıf yolundan üretiliyorsa, içine `Iddia` ya da `DepoTest` gibi bir tip hiç girmiyor;
girerse, o çıktı gereğinden büyük ve gereğinden çok tip taşıyor demektir. Test sınıf yolunun
üçüncü bir yol olması, üretim çıktısına karışmaması gerektiği anlamına da gelir — iki yolun
birbirine karışması, sızmış bir bağımlılığın en sık görülen biçimidir.

Üç sınıf yolunu yan yana koyunca sıralama netleşiyor: derleme sınıf yolu `DepoTest`'i derlemek
için `Depo` ile `Iddia`'yı ister, koşum sınıf yolu yalnız `Depo`'yu taşır ve `Iddia`'dan hiç
haberdar değildir, test sınıf yolu ise üçünü birden taşır. Üç yol da aynı diskteki aynı
`.class` dosyalarına işaret edebilir; aralarındaki fark dosyaların içeriğinde değil, hangi
komuta hangi dizinlerin verildiğindedir. Bir tipin "test sınıf yolunda gerekli" olması, o
tipin üretim kodunda hiç yazılmadığı, yalnız testin kendi ihtiyacından doğduğu anlamına gelir
— ve bu ayrım, iki yolu gerçekten ayrı ayrı kurmadan görünmez.

Bu derste `Iddia` ve `DepoTest` derleme zamanında birbirine bağımlıydı, ama koşum zamanında
`Iddia`'nın **koşum sınıf yolunda** bulunması derlemeden ayrı bir zorunluluktu — nitekim son
satır bunu ayırıyor. Sıradaki ders bu gözlemi bir adım öteye taşıyor: testin ihtiyaç duyduğu
tip, sınanan kodun **zaten** kullandığı bir tipin yerine geçtiğinde, aynı derlenmiş test iki
ayrı koşum sınıf yoluyla iki ayrı sonuç verebilir.

Son iki satır bunu koşarak gösteriyor. `DepoTest` tam sınıf yoluyla — üretim, çerçeve ve test
tipleri bir arada — çalıştırıldığında `Iddia.esit` çağrısı bulunuyor ve olgu "tamam" dönüyor.
Aynı `DepoTest.class` dosyası, bu kez yalnız üretim ve test dizinleriyle, çerçeve dizini
**olmadan** çalıştırıldığında yükleyici `Iddia` adını hiçbir yerde bulamıyor ve
`NoClassDefFoundError` fırlatıyor. Dosyanın kendisi değişmedi, yeniden derlenmedi; yalnız
kendisine verilen sınıf yolu daraldı. Bu, "test sınıf yolu üçüncü bir yoldur" iddiasının en
somut kanıtı: çerçevenin sağladığı tip, testin **derlenmesi** için de **koşması** için de
gerekli, ama üretimin ne derlenmesi ne koşması için gerekli.

## Özet

- Test çerçevesi burada kurulum, gövde ve sökme çağrılarını sırayla yapan, iddia
  başarısızlığını bir istisnayla yakalayan küçük bir koşturucu olarak modellendi.
- Aynı iddia sökmeden önce kontrol edildiğinde geçti, sökmeden sonra kontrol edildiğinde
  düştü; koşturucu ikisini de "koştu" saydı, sıranın kendisi doğruluğu garanti etmedi.
- Üretim sınıf yolu tek bir tip taşıdı; test sınıf yolu aynı tipe ek olarak çerçevenin ve
  testin kendi tiplerini taşıdı — toplam üç tip.
- Aynı derlenmiş `DepoTest.class`, çerçeve tipi eksik bir sınıf yoluyla koşturulunca yeniden
  derlenmeden `NoClassDefFoundError` verdi; dosya değişmedi, yalnız sınıf yolu değişti.
- Test sınıflandırması ve iddia yazımı M21/K02 Birim Testi ve Test Odaklı Geliştirme
  kursunda kuruldu ve burada tekrarlanmadı.

## Sonraki Adım

Bu derste test sınıf yoluna giren tipler kendi elimizle yazdığımız, sınanan koddan tamamen
ayrı duran tiplerdi. Ama testin ihtiyaç duyduğu bir bağımlılık, üretim kodunun **zaten**
kullandığı bir tipin yerini almak zorunda kalabilir — aynı adı taşıyan, aynı imzayı sağlayan
ama farklı davranan bir tip. Sıradaki ders aynı derlenmiş testi, gerçek tipin durduğu bir
sınıf yoluna karşı bir kez, aynı adı taşıyan sahte tipin durduğu başka bir sınıf yoluna karşı
bir daha koşturur ve test yeniden derlenmeden iki ayrı sonuç aldığını sayar.
