Ders 05 / 12
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.
İçindekiler
Ö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
Runnableolarak 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
AssertionErrorfı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.
// 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ş
.classdosyaları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.classdosyası ü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.
// 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 derlenmedenNoClassDefFoundErrorverdi; 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.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.