Ders 08 / 12
Tümleşim Testleri
Birim kurulumunda sahtelenen tedarikçi burada gerçek gerçekleştirimiyle, gerçek gerçekleştirimin bağlı olduğu ikinci bir tiple birlikte koşum sınıf yoluna geri konuyor; yolun içerdiği tip sayısı bir tipten ikiye çıkıyor. Aynı test gövdesi iki kurulumda koşturuluyor: birimde sınır aşımı sessizce geçiyor, tümleşimde aynı istek koşum hatası veriyor.
İçindekiler
Önceki derste paylaşılan olan, bir test gövdesinin kendi kurduğu bir nesneydi; sınanan kodun
kendisi hiç değişmedi. Bu ders paylaşılan durumu sınanan kodun dışına taşıyor: ikinci
derste sahtelenen Tedarikci bağımlılığı burada gerçek gerçekleştirimiyle geri geliyor, ve
gerçek gerçekleştirimin kendi bağlı olduğu ikinci bir tip de sınıf yoluna eklenmiş oluyor.
Soru şu: aynı test gövdesi, içeriği büyüyen bir sınıf yoluna karşı koşturulduğunda, önceden
sessiz kalan bir sapma nereye düşüyor?
Dördüncü dersin ekseni, ikinci dersle aynı Tedarikci adı üzerinden ilerlese de farklı bir
soruya yanıt arıyor. İkinci ders “aynı ad, iki gövde” sorusunu sormuştu ve bir tipin
diğerinin yerine geçmesinin ne zaman sessiz, ne zaman koşum hatası verdiğini ölçmüştü. Bu
ders o sorunun bir adım gerisine gidiyor: gerçekleştirimlerden biri, ötekinin taşımadığı
bir bağımlılık taşıdığında, sınıf yolunun kendisi nasıl değişiyor ve o değişiklik hangi
kusuru görünür kılıyor?
Tümleşim testinin konu alanı — test ortamı yönetimi, dış servislerin sahtelenmesi — M21/K03 Tümleşim, Sözleşme ve Uçtan Uca Test kursunda kuruldu ve burada yeniden kurulmaz. Bu dersin eklediği tek soru, testin gördüğü sınıf yolunun büyüklüğü ile kusurun düştüğü yer arasındaki ilişkidir.
Birim ve tümleşim kurulumları arasındaki fark çoğunlukla kurulum süresi ya da dış sistemlere erişim üzerinden anlatılır — biri hızlı ve yalıtılmış, öteki yavaş ve gerçek. Bu ders aynı farkı sınıf yolu üzerinden okuyor: iki kurulum arasındaki gerçek fark, testin derleme zamanında hiç görmediği ama koşum zamanında karşılaştığı tip kümesidir. Kurulumu “birim” ya da “tümleşim” yapan şey bir etiket değil, o koşumda sınıf yoluna hangi tiplerin konduğudur.
Aynı Bağımlılık, Gerçek Gerçekleştirimiyle
- TS16. Dış sistem burada
Envanteradlı, gerçek dünyadaki bir stok sınırını modelleyen sıradan bir Java sınıfı olarak kurulur. Hiçbir ürün, sürücü ya da protokol adı yazılmaz; gerçek bir ağ çağrısı, dosya sistemine erişim ya da dış süreç de yapılmaz. - TS17. “Yolun içerdiği tip sayısı” burada derlenmiş
.classdosyalarının sayımıdır — ilk derste kurulan ölçüm yöntemiyle aynı.
İkinci derste Tedarikci adlı bir tip, istenen miktarın tamamını karşılayan bir üretim
gerçekleştirimiyle ve isteneni bir eksik karşılayan bir sahte gerçekleştirimle vardı; test
ikisine de derleme zamanında bağlı değildi, yalnız adı yansımayla arıyordu. Bu ders aynı adı
tekrar kullanıyor, ama gerçek gerçekleştirimi bu kez yalnız başına değil, kendi bağlı
olduğu bir sınır denetimiyle birlikte kuruyor: Envanter.sinirDenetle, istenen miktar
100’ü aştığında bir istisna fırlatıyor. Birim kurulumundaki sahte gerçekleştirim böyle bir
denetim taşımıyor — her isteği sessizce karşılıyor.
Bu iki gerçekleştirimin farkı bilinçli bir tasarım kararı. Birim kurulumu bir sınır
denetimi taşımıyor, çünkü birim testinin amacı Tedarikci’yi çağıran kodun akışını
sınamaktır — sınırın kendisi Envanter’in sorumluluğundadır ve Envanter’in kendi testi
ayrı yazılır. Tümleşim kurulumu ise iki tipi birlikte çalıştırarak, aralarındaki gerçek
etkileşimi sınıyor: Tedarikci çağrıyı Envanter’e gerçekten iletiyor mu, Envanter’in
fırlattığı istisna gerçekten çağıranı buluyor mu? Bu soruların ikisi de birim kurulumunda
sorulamaz, çünkü Envanter orada hiç yok.
// Buyuyen.java — tumlesim kurulumu kosum sinif yoluna gercek tipleri geri koyar, yolun icerdigi tip sayisi buyur
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 Buyuyen {
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, Buyuyen.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("buyuyen");
Path kaynak = kok.resolve("kaynak");
Path testD = kok.resolve("test"), birimD = kok.resolve("birim"), tumlesimD = kok.resolve("tumlesim");
Path sTest = yaz(kaynak, "TumlesimTest",
"import java.lang.reflect.InvocationTargetException;\n"
+ "import java.lang.reflect.Method;\n"
+ "public class TumlesimTest {\n"
+ " public static void main(String[] a) throws Throwable {\n"
+ " Class<?> c = Class.forName(\"Tedarikci\");\n"
+ " Object nesne = c.getDeclaredConstructor().newInstance();\n"
+ " Method m = c.getMethod(\"dolum\", int.class);\n"
+ " int[] istekler = {30, 150};\n"
+ " for (int istek : istekler) {\n"
+ " try {\n"
+ " int sonuc = (int) m.invoke(nesne, istek);\n"
+ " System.out.println(\"istek \" + istek + \" -> sonuc \" + sonuc);\n"
+ " } catch (InvocationTargetException e) {\n"
+ " throw e.getCause();\n"
+ " }\n"
+ " }\n"
+ " System.out.println(\"tum istekler tamamlandi\");\n"
+ " }\n"
+ "}\n");
Path sBirim = yaz(kaynak.resolve("birim"), "Tedarikci",
"public class Tedarikci {\n"
+ " public int dolum(int istenen) { return istenen; }\n"
+ "}\n");
Path envDizin = kaynak.resolve("tumlesim");
Path sEnvanter = yaz(envDizin, "Envanter",
"public class Envanter {\n"
+ " static void sinirDenetle(int istenen) {\n"
+ " if (istenen > 100) throw new IllegalStateException(\"stok siniri asildi\");\n"
+ " }\n"
+ "}\n");
Path sGercek = yaz(envDizin, "Tedarikci",
"public class Tedarikci {\n"
+ " public int dolum(int istenen) { Envanter.sinirDenetle(istenen); return istenen; }\n"
+ "}\n");
derle(testD, List.of(), List.of(sTest));
derle(birimD, List.of(), List.of(sBirim));
derle(tumlesimD, List.of(), List.of(sEnvanter, sGercek));
System.out.println("-- yolun icerdigi tip sayisi --");
System.out.println("birim kurulumu : " + tipSayisi(birimD) + " tip");
System.out.println("tumlesim kurulumu : " + tipSayisi(tumlesimD) + " tip");
System.out.println("-- ayni test govdesi, iki kurulum --");
System.out.println("birim kosum sonucu : " + kos(List.of(testD, birimD), "TumlesimTest"));
System.out.println("tumlesim kosum sonucu: " + kos(List.of(testD, tumlesimD), "TumlesimTest"));
}
}
-- yolun icerdigi tip sayisi -- birim kurulumu : 1 tip tumlesim kurulumu : 2 tip -- ayni test govdesi, iki kurulum -- istek 30 -> sonuc 30 istek 150 -> sonuc 150 tum istekler tamamlandi birim kosum sonucu : tamam istek 30 -> sonuc 30 tumlesim kosum sonucu: IllegalStateException
Birim kurulumu tek bir .class dosyası taşıyor: sahte Tedarikci, başka hiçbir şeye bağlı
değil. Tümleşim kurulumu iki .class dosyası taşıyor: gerçek Tedarikci ve onun çağırdığı
Envanter. TumlesimTest üçüncü kez de aynı dosya, aynı bayt kodu — değişen yalnız hangi
dizinlerin sınıf yoluna verildiği. Yolun içeriği büyüyor çünkü gerçek gerçekleştirim, sahte
gerçekleştirimin hiç taşımadığı bir bağımlılığı kendisiyle birlikte getiriyor.
Bu büyüme, birinci derste sayılan büyümeden farklı bir yönde ilerliyor. Orada test sınıf
yolu, üretim sınıf yolunun hiç tanımadığı iki tip taşıyordu — çerçeve ve testin kendisi,
ikisi de üretim kodunun dışındaydı. Burada büyüyen taraf üretim tarafı: Envanter, üretim
kodunun gerçek bir parçasıdır, ama birim kurulumunda hiç sınıf yoluna girmedi. Bir sınıf
yolunun büyümesi tek bir anlama gelmiyor — kimi zaman test kendi araçlarını taşıdığı için
büyüyor, kimi zaman üretimin gerçek bir parçasını geri kattığı için. İkisi de aynı sayma
yöntemiyle — derlenmiş .class dosyalarını saymak — ölçülüyor, ama işaret ettikleri şey
ayrı.
Aynı Test Gövdesi, İki Kurulum, İki Kusur Sınıfı
- TS18. Aynı istek dizisi —
30ve150— hem birim hem tümleşim kurulumuna karşı, test yeniden derlenmeden koşturulur. - TS19. Testin kendisi burada bir iddia taşımıyor, yalnız çağrıları yapıp sonucu yazdırıyor. Bu, “sessiz” sonucu daha da keskinleştiriyor: bir iddia eklenseydi bile o iddia yalnız dönüş değerine bakardı, sınır ihlalinden habersiz kalırdı.
- TS20. Gerçek gerçekleştirimin attığı istisna işaretlenmemiş (unchecked) bir
istisnadır. Yansımalı çağrı bunu
InvocationTargetExceptioniçine sarar; test kaynağı bu sarmalayıcıyı açıp asıl nedeni yeniden fırlatır, aksi hâlde harness yalnız sarmalayıcı sınıfının adını görürdü.
İki istek de aynı sırayla gönderiliyor: önce 30, sonra 150. Birim kurulumunda ikisi de sorunsuz dönüyor — sahte gerçekleştirim hiçbir sınır tanımıyor, “istek 150 -> sonuc 150” satırı diğerleriyle aynı biçimde yazılıyor ve program “tum istekler tamamlandi” diyerek bitiyor. Sınırın var olduğu bilgisi hiçbir yerde görünmüyor; test geçti, çünkü test zaten sınırdan habersizdi.
Tümleşim kurulumunda ilk istek (30) aynı şekilde dönüyor, ama ikinci istek (150) Envanter
sınırını aşıyor ve Tedarikci.dolum bir istisna fırlatıyor. Bu istisna testin kendi
kaynağında yakalanıp yeniden fırlatılıyor, sonra koşumu üstlenen yardımcı bunu yakalayıp
adını raporluyor: IllegalStateException. Aynı iki istek, aynı sırayla, aynı test dosyası
— fark yalnız hangi Tedarikci’nin ve hangi ek bağımlılığın sınıf yolunda durduğu.
Bu istisnanın testin kendi kaynağından çıkışı iki katmanlıdır ve bu ders bilerek bu iki
katmanı ayırıyor. Tedarikci.dolum bir IllegalStateException fırlatıyor; test bunu
m.invoke(nesne, istek) çağrısıyla yansıma üzerinden tetiklediği için, yansımanın kendisi
bu istisnayı önce bir InvocationTargetException’ın içine sarıyor. Test kaynağı bu
sarmalayıcıyı catch (InvocationTargetException e) ile yakalayıp throw e.getCause()
diyerek asıl istisnayı açığa çıkarıyor ve yeniden fırlatıyor. Bu adım atlansaydı, koşumu
üstlenen yardımcı yalnız “InvocationTargetException” adını raporlardı — doğru ama az şey
söyleyen bir isim. Sarmalayıcının testin kendi kaynağında açılması, raporlanan adın
IllegalStateException olmasını, yani neyin yanlış gittiğinin bir adım daha görünür
kalmasını sağlıyor.
Bu iki sonuç, kursun sınırlayıcı okumasının test konusundaki karşılığı: gerçek tipi geri koymak testi daha doğru yapmıyor, yalnız başka bir kusur sınıfını görünür kılıyor. Birim kurulumu zaten geçerli bir kurulumdu — hızlı, yalıtılmış, gerçek bir sınırın varlığından bağımsız. Tümleşim kurulumu onu yanlışlamıyor, üstüne bir katman ekliyor: gerçek gerçekleştirimin taşıdığı kısıtları da sınıf yoluna dahil ediyor. Birim kurulumunda “sessiz” kalan sapma — sınırı aşan bir isteğin fark edilmeden geçmesi — burada bir istisnaya, yani “koşum” kusuruna dönüşüyor. İkisi de aynı iki isteği, aynı sırayla işledi; aralarındaki fark testin yazdığı iddialarda değil, testin koştuğu sınıf yolunun içeriğindeydi. İki kurulum da doğru kurulmuş olabilir; ayırdıkları şey aynı girdiye verdikleri yanıt değil, o girdinin hangi gerçekliğe karşı sınandığıdır.
Bu ayrımın pratik sonucu, hangi kurulumun “yeterli” olduğuna dair tek bir yanıt olmadığıdır.
Birim kurulumu 150 gibi bir isteğin normal akışta işlenip işlenmediğini hızlıca doğrular;
Envanter’in kendisi değiştiğinde ya da hiç var olmadığında bile çalışmaya devam eder,
çünkü ona hiç bağlı değildir. Tümleşim kurulumu ise Envanter’in gerçekten çağrıldığını ve
gerçekten bir sınırı uyguladığını doğrular, ama bunun bedeli sınıf yoluna ikinci bir tipi
dahil etmek ve o tipin davranışına bağımlı hâle gelmektir. Hangi sorunun sorulduğu, hangi
kurulumun seçileceğini belirliyor; ikisi de aynı Tedarikci adını taşıyan farklı
gerçeklikleri sınıyor.
İki kurulumun birbirinin yerini almadığını, ikisinin de gerekli kaldığını vurgulamak
gerekiyor. Birim kurulumu hâlâ
değerli: Envanter gibi ağır ya da yavaş bir bağımlılık her koşumda gerçekten kurulsaydı,
Tedarikci’yi çağıran kodun kendi mantığını sınamak her seferinde o bağımlılığın kurulma
maliyetini de taşırdı. Birim kurulumu bu maliyeti tamamen ortadan kaldırıyor ve karşılığında
yalnız Envanter’in var olmadığı bir dünyayı sınıyor. Tümleşim kurulumu ise o maliyeti
göze alıp iki tipin birlikte nasıl davrandığını gerçekten koşturuyor. Bir test takımı
genellikle ikisini bir arada tutar: çok sayıda birim kurulumu her değişiklikte hızlıca
koşar, daha az sayıda tümleşim kurulumu ise gerçek bağımlılıkların bir arada çalıştığını
daha seyrek doğrular. Bu dersin gösterdiği, iki katmanın aynı davranışı iki kez
sınamadığı — her biri kendi göremediği bir kusur sınıfını taşıdığıdır.
Bu noktada sınıf yolu ölçüsü somut bir karar aracına dönüşüyor: bir kurulumun “birim” mi
“tümleşim” mi olduğunu tartışmak yerine, o kurulumun sınıf yoluna hangi tiplerin girdiğine
bakmak yeterli. Envanter sınıf yolundaysa, Envanter’in davranışı sınanıyor demektir;
sınıf yolunda değilse, sınanmıyor demektir — aradaki fark bir niyet beyanı değil, derlenmiş
bir gerçektir ve tipSayisi gibi bir sayımla doğrudan okunabilir.
Bu ölçü aynı zamanda bir uyarı taşıyor: sınıf yoluna eklenen her gerçek tip, testin
gördüğü dünyayı üretimin gördüğü dünyaya biraz daha yaklaştırıyor, ama hiçbir zaman
tamamen eşitlemiyor. Envanter‘in kendisi de başka bir şeye bağlı olabilirdi — bu derste
öyle kurulmadı, sınırın kendisi kendi içinde tam. Gerçek bir sistemde tümleşim kurulumu da
kendi sınırında durur; bir noktadan sonra sınanan sistemin tamamını ayağa kaldırmak
tümleşim testinin değil, uçtan uca testin işidir, ve o sınır M21/K03’ün konusudur.
Özet
- Birim kurulumu tek bir tip taşıdı, tümleşim kurulumu gerçek gerçekleştirimle birlikte onun bağlı olduğu ikinci bir tipi de taşıdı; yolun içeriği bire karşı ikiye çıktı.
- Aynı derlenmiş test gövdesi, aynı iki istekle, iki ayrı koşum sınıf yoluna karşı yeniden derlenmeden koşturuldu.
- Birim kurulumunda sınırı aşan istek sessizce geçti ve test “tamamlandı” dedi; hiçbir yerde bir uyarı çıkmadı.
- Tümleşim kurulumunda aynı istek
IllegalStateExceptionfırlattı; kusur artık sessiz değil, koşum hatasıydı. - Gerçek gerçekleştirimi geri koymak testi daha doğru yapmadı, yalnız birim kurulumunda görünmeyen bir kusur sınıfını görünür kıldı.
Sonraki Adım
Bu derste test bir sayı gönderdi ve dönen değeri okudu; iddia gövdenin içinde, düzyazı olmayan bir biçimdeydi. Bazı testler ise gövdeye hiç düzyazı olmayan bir kodla değil, senaryo dilinde yazılmış bir metinle yaklaşır — adımları sıradan cümlelerle anlatır ve o cümlelerin bir karşılığı olmasını bekler. Bu, testin gördüğü sınıf yolu sorusunu bir kez daha başka bir yöne taşıyor: artık iki kod tabanı arasındaki bir uyumsuzluk değil, düzyazı bir metin ile o metnin çalıştırılabilir karşılığı arasındaki uyumsuzluk aranıyor. Sıradaki ders böyle bir metni bir bildirim olarak ele alıyor ve sorusu şu: metindeki her adımın çalıştırılabilir bir karşılığı var mı, ve bazı adımların bu karşılığı bulunamadığında sessizce mi atlandığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.