Ders 06 / 12
Sahteleme
Aynı derlenmiş test, gerçek tedarikçi tipinin durduğu bir koşum sınıf yoluna karşı bir kez, aynı adı taşıyan sahte tipin durduğu başka bir koşum sınıf yoluna karşı bir daha çalıştırılır. Test yeniden derlenmeden iki ayrı sonuç veriyor, hata yok; imza ayrıldığında ise aynı kurulum koşumda düşüyor ve nedeni test metninde görünmüyor.
İçindekiler
Önceki derste test sınıf yoluna giren tipler — çerçeve ve testin kendisi — sınanan koddan tamamen ayrı duruyordu; üretim kodu onları hiç anmıyordu. Ama testin sık sık yerine koyduğu bir bağımlılık, üretim kodunun zaten kullandığı bir tipin yerini alır: aynı adı taşır, aynı çağrıyı yanıtlar, ama farklı davranır. Bu ders aynı derlenmiş testi böyle bir tipin iki ayrı gerçekleştirimine karşı çalıştırıyor — biri gerçek, öteki sahte — ve testi hiç yeniden derlemeden iki ayrı sonuç aldığını sayıyor. Bu, önceki dersin sınadığı boşluğun tam tersi yönde bir boşluk: orada bir tip hiç bulunmadığında ne olduğuna bakıldı, burada bir tip başka bir tipin yerine bulunduğunda ne olduğuna bakılıyor.
Sahte bağımlılık ailesi — sahte nesne, güdük, casus nesne, kukla nesne — M21/K02 Birim Testi ve Test Odaklı Geliştirme kursunda kuruldu ve burada yeniden tanıtılmaz; hangi türün hangi soruyu yanıtladığı orada yazılı. Bu dersin eklediği tek soru farklı bir eksendedir: sahte tip hangi sınıf yolunda duruyor, ve gerçek tipin yerine geçmesi derleme zamanında mı, koşum zamanında mı oluyor?
Bu soru, önceki derste kurulan test sınıf yolu sorusundan farklı bir yerde duruyor. Orada
test sınıf yolu, üretim sınıf yolunun hiç tanımadığı iki tip taşıyordu — çerçeve ve testin
kendisi. Burada durum tersine dönüyor: sahte tip, test kodunun zaten bir adla andığı bir
bağımlılığın yerine geçiyor. Test kodu Tedarikci adını yazdı, o adın hangi sınıf dosyasıyla
karşılık bulacağı ise sınıf yoluna bırakıldı. Aynı ad, aynı çağrı, iki ayrı gövde — ve hangi
gövdenin çalışacağına karar veren şey kaynak metin değil, o koşumda sınıf yoluna hangi
dizinin verildiğidir.
Aynı Adı Taşıyan İki Gerçekleştirim
- TS6. Sahte tip burada aynı ada ve uyumlu bir imzaya sahip, elle yazılmış ayrı bir sınıftır. Sahteleme kitaplıklarının çalışma zamanında bayt kodu üreten vekil (proxy) düzeneği burada kurulmaz — davranış aynı sonucu elle yazılmış bir sınıfla üretir.
- TS7. Test kaynağı
Tedarikciadını yalnız yansımayla (Class.forName) arar; kaynağında hiçbir gerçekleştirime derleme zamanında bağlı değildir. Bu yüzden test bir kez derlenir ve farklı koşum sınıf yollarıyla değişmeden çalışır.
Sınanan davranış bir depo dolum çağrısıdır: Tedarikci adlı bir tip, istenen miktarı alır ve
ne kadarının karşılandığını döndürür. Üretim kodunda bu çağrının arkasında gerçek bir tedarik
süreci durur — burada modellenmiyor, yalnız çağrının kendisi ve döndürdüğü sayı önemli. Bir
testin bu çağrıyı her koştuğunda gerçek tedariğe gitmesi hem yavaş hem kararsız olurdu; bu
yüzden test, gerçek gerçekleştirimin yerine geçebilecek, aynı adı ve aynı çağrı biçimini
taşıyan başka bir tipe ihtiyaç duyuyor. Üç ayrı gerçekleştirim yazıyoruz. Üretim
gerçekleştirimi istenen miktarın tamamını karşılıyor. Sahte gerçekleştirim her çağrıda bir
eksik karşılıyor ve kaç kez çağrıldığını statik bir alanda tutuyor — casus nesnenin çağrı
kaydını burada bu alan üstleniyor. Üçüncü gerçekleştirim ise aynı adı taşıyor ama ilk
parametresinin tipini int’ten long’a değiştiriyor; bu, imza uyuşmazlığının koşuma ne
yaptığını göstermek için sonradan kullanılıyor. Test, üç gerçekleştirimin hiçbirine
derleme zamanında bağlı değildir — yalnız Tedarikci adını ve dolum(int) imzasını yansıma
üzerinden arar.
// IkiKosum.java — ayni derlenmis test, gercek tip ve ayni adli sahte tip arasinda kosum sinif yolu degisince farkli sonuc verir
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 IkiKosum {
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, IkiKosum.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();
}
}
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("ikikosum");
Path kaynak = kok.resolve("kaynak");
Path testD = kok.resolve("test"), uretimD = kok.resolve("uretim"),
sahteD = kok.resolve("sahte"), bozukD = kok.resolve("bozuk");
Path sTest = yaz(kaynak, "TedarikTest",
"import java.lang.reflect.Field;\n"
+ "import java.lang.reflect.Method;\n"
+ "public class TedarikTest {\n"
+ " public static void main(String[] a) throws Exception {\n"
+ " Class<?> c = Class.forName(\"Tedarikci\");\n"
+ " Object nesne = c.getDeclaredConstructor().newInstance();\n"
+ " Method m = c.getMethod(\"dolum\", int.class);\n"
+ " int[] istekler = {5, 3, 8};\n"
+ " int toplam = 0;\n"
+ " for (int istek : istekler) toplam += (int) m.invoke(nesne, istek);\n"
+ " int beklenen = 5 + 3 + 8;\n"
+ " System.out.println(\"beklenen : \" + beklenen);\n"
+ " System.out.println(\"gelen : \" + toplam);\n"
+ " System.out.println(\"iddia : \" + (toplam == beklenen));\n"
+ " try {\n"
+ " Field f = c.getField(\"cagriSayisi\");\n"
+ " System.out.println(\"cagri sayisi: \" + f.getInt(null));\n"
+ " } catch (NoSuchFieldException e) {\n"
+ " System.out.println(\"cagri sayisi: izlenmiyor\");\n"
+ " }\n"
+ " }\n"
+ "}\n");
Path sUretim = yaz(kaynak.resolve("uretim"), "Tedarikci",
"public class Tedarikci {\n"
+ " public int dolum(int istenen) { return istenen; }\n"
+ "}\n");
Path sSahte = yaz(kaynak.resolve("sahte"), "Tedarikci",
"public class Tedarikci {\n"
+ " public static int cagriSayisi = 0;\n"
+ " public int dolum(int istenen) { cagriSayisi++; return istenen - 1; }\n"
+ "}\n");
Path sBozuk = yaz(kaynak.resolve("bozuk"), "Tedarikci",
"public class Tedarikci {\n"
+ " public int dolum(long istenen) { return (int) istenen; }\n"
+ "}\n");
derle(testD, List.of(), List.of(sTest));
derle(uretimD, List.of(), List.of(sUretim));
derle(sahteD, List.of(), List.of(sSahte));
derle(bozukD, List.of(), List.of(sBozuk));
System.out.println("-- gercek tiple --");
System.out.println("kosum sonucu: " + kos(List.of(testD, uretimD), "TedarikTest"));
System.out.println("-- sahte tiple (test yeniden derlenmedi) --");
System.out.println("kosum sonucu: " + kos(List.of(testD, sahteD), "TedarikTest"));
System.out.println("-- imza degisen tiple --");
System.out.println("kosum sonucu: " + kos(List.of(testD, bozukD), "TedarikTest"));
}
}
-- gercek tiple -- beklenen : 16 gelen : 16 iddia : true cagri sayisi: izlenmiyor kosum sonucu: tamam -- sahte tiple (test yeniden derlenmedi) -- beklenen : 16 gelen : 13 iddia : false cagri sayisi: 3 kosum sonucu: tamam -- imza degisen tiple -- kosum sonucu: NoSuchMethodException
TedarikTest bir kez derleniyor (testD) ve üç kez farklı bir ikinci sınıf yolu bileşeniyle
koşturuluyor: önce uretimD, sonra sahteD, en son bozukD. Aradaki fark yalnız kos
çağrısına verilen listenin ikinci elemanı; testin kendi .class dosyası üçünde de birebir
aynı bayt kodu taşıyor.
TedarikTest’in derlenmesi hiçbir gerçekleştirime ihtiyaç duymuyor — kaynağında Tedarikci
adı yalnız bir dize sabiti olarak geçiyor, import edilen bir tip olarak değil. Bu, birinci
derste kurulan derleme sınıf yolu ile koşum sınıf yolu ayrımını daha da keskinleştiriyor:
burada test hiçbir derleme sınıf yolu bileşenine ihtiyaç duymadan derleniyor, ama üç ayrı
koşumun her biri için farklı bir koşum sınıf yolu bileşenine ihtiyaç duyuyor. Derleme zamanında
görünmeyen bir bağımlılık, koşum zamanında zorunlu hâle geliyor — bu da geçen konunun
kurduğu “derlendi, bir kanıt değildir” okumasının test sınıf yolundaki karşılığı.
Gerçek sahteleme kitaplıkları bu değişimi genellikle arayüz tabanlı yapar: sınanan kod bir arayüze bağlanır, sahte tip o arayüzü uygular ve derleyici ikisinin uyumunu denetler. Burada seçilen yol daha çıplak — doğrudan yansıma — çünkü ders üçüncü sınıf taraf bir çatı adı yazmadan aynı sonucu üretmek zorunda. Arayüz tabanlı yaklaşımda da sonuç aynıdır: hangi gerçekleştirimin kullanılacağı yine koşum sınıf yoluna bırakılır, derleyici yalnız bir gerçekleştirimin var olduğunu, kaç tane olduğunu ve hangisinin seçileceğini denetlemez.
Sessiz Ayrışma: Hata Yok, Sonuç Farklı
- TS8. Çağrı kaydı burada statik bir alan (
cagriSayisi) olarak tutulur. Her koşum kendiURLClassLoader’ını kullandığı için bu alan her koşumda sıfırdan başlar; iki koşum arasında sayaç birbirine karışmaz.
Gerçek tiple koşulan ilk çalıştırma beklendiği gibi geçiyor: üç istek toplamı 16, dönen toplam
da 16, iddia doğru. İkinci çalıştırma aynı test dosyasını, aynı bayt kodla, yalnız ikinci
sınıf yolu bileşenini sahteD yaparak koşturuyor. Sahte gerçekleştirim her çağrıda isteneni
bir eksik karşılıyor; üç çağrının toplamı bu yüzden 13’e düşüyor ve iddia satırı false
basıyor. Burada hiçbir istisna fırlamıyor, hiçbir derleme hatası yok — kos çağrısı ikisinde
de “tamam” döndürüyor. Fark yalnız bir satırın içeriğinde: iddia alanı true’dan false’a
düşüyor. Bu, kursun sessiz kusur ailesinin en temiz örneği: iki koşum da başarıyla tamamlanıyor,
ikisi de “çalıştı” sayılıyor, ama ürettikleri sonuç aynı değil.
Casus nesnenin tuttuğu çağrı kaydı bu ayrımı doğruluyor. Üretim gerçekleştiriminde
cagriSayisi diye bir alan yok — test bunu yakalıyor ve “izlenmiyor” yazıyor. Sahte
gerçekleştirimde ise aynı alan 3 değerini taşıyor: dolum üç kez çağrılmış, üçü de kaydı
artırmış. Bu sayı, testin dönüş değerine hiç bakmadan da tek başına bir kanıt taşır — sahte
tip gerçekten üç kez çağrıldı, ne bir eksik ne bir fazla. iddia alanı ne döndüğünü
söylüyor, cagriSayisi kaç kez çağrıldığını söylüyor; ikisi ayrı sorulara yanıt veriyor
ve ikisi de aynı koşumda birlikte okunabiliyor.
Bu iki koşumun ürettiği fark, bir test takımının “geçti” demesinin neyi garanti etmediğini gösteriyor. Gerçek gerçekleştirimle koşulan takım geçiyor, sahte gerçekleştirimle koşulan takım düşüyor — ikisi de aynı test dosyasını, aynı iddiayı taşıyor. Eğer bir dağıtım hattı testleri yalnız sahte gerçekleştirimin durduğu bir sınıf yoluyla çalıştırıyorsa, üretim gerçekleştirimindeki bir bozulmayı hiç görmez; testler geçmeye devam eder çünkü hiçbiri gerçek tiple hiç koşmamıştır. Bu, kursun ölçü sorusunun test konusundaki en doğrudan karşılığı: bir testin “geçmesi”, o testin hangi sınıf yoluyla koştuğuna bakılmadan okunamaz.
İmza Ayrıldığında Kusur Koşuma Düşer
- TS9. İmza karşılaştırması burada yalnız ilk parametrenin tipine bakıyor
(
getMethod("dolum", int.class)); gerçek bir yansıma araması adı, parametre sayısını ve parametre tiplerini birlikte eşleştirir, dönüş tipine bakmaz. - TS10. Sahteleme kitaplıklarının doğrulama (verify) çağrısı burada kurulmaz; casus nesnenin ürettiği sinyal bu derste yalnız okunan bir sayaçtır, kendi kendini doğrulayan bir nesne değildir.
Üçüncü koşum aynı TedarikTest.class’ı, bu kez dolum’un ilk parametresi int değil long
olan bir gerçekleştirimin durduğu sınıf yoluyla çalıştırıyor. Class.forName("Tedarikci")
sınıfı buluyor — ad aynı, sınıf gerçekten var — ama c.getMethod("dolum", int.class) çağrısı
tam olarak bu imzayla bir yöntem bulamıyor ve NoSuchMethodException fırlatıyor. Bu istisna
testin kendi main’inden yakalanmadan çıkıyor, kos yardımcısı bunu yakalayıp adını
döndürüyor: kosum sonucu: NoSuchMethodException.
Buradaki ayrım önceki iki koşumdan farklı bir kusur sınıfına düşüyor. Sahte tiple koşum
sessizdi — iki taraf da geçerli bir sonuç üretti, farkı yalnız bir sayı taşıdı. İmza
ayrılan tiple koşum ise koşumda düşüyor: program bir istisnayla duruyor. Ama bu istisnanın
neyi anlattığına dikkat etmek gerekir — NoSuchMethodException mesajı hangi sınıfın hangi
yöntemi aradığını söyler, ama neden o yöntemin artık orada olmadığını söylemez. Test
metninin kendisi hiç değişmedi; kusurun nedeni yalnız değiştirilen gerçekleştirimin
kaynağında görünür, test dosyasında değil. Bir sınama koşum sınıf yolunda hangi tipin
durduğuna bu kadar bağlıysa, imza uyuşmazlığı derlemede değil koşumda yakalanıyor ve o anda
elde yalnız bir istisna sınıfı adı kalıyor.
Bu üç koşumun sıralaması bilinçli: gerçek tip, sonra uyumlu sahte tip, en son uyumsuz tip. Aradaki fark sahte tipin ne kadar gerçek tipe benzediğidir. Uyumlu sahte tip, imzası gerçek tipin imzasıyla birebir eşleştiği için testin hiç haberi olmadan yerine geçebiliyor — kusuru yalnız sonuçtaki bir sayı taşıyor. Uyumsuz tip ise adı aynı olsa da imzası ayrıldığı için testin yansıma çağrısı onu hiç bulamıyor — kusur artık bir sayı değil, bir istisna. Bu iki uç arasındaki fark, sahtelemenin gerçek riskini gösteriyor: sahte tipi güncel tutmak yalnız adını değil, imzasını da gerçek tiple eşleşir tutmayı gerektiriyor, ve bu eşleşme derleyici tarafından denetlenmiyor çünkü ikisi hiçbir zaman aynı derlemede bir arada bulunmuyor.
Üç koşumun üçü de aynı üç kusur sınıfını örnekliyor: gerçek tiple koşum sorunsuz, sahte tiple koşum sessizce farklı bir sonuç veriyor, imzası ayrılan tiple koşum bir istisnayla düşüyor. Hiçbiri derleme sırasında yakalanmadı — üçü de derleme sınıf yolunda hiç görünmeyen bir kararın koşum sınıf yolunda ortaya çıkmasıydı. Bir test takımının güvenilirliği bu yüzden yalnız hangi iddiaları yazdığına değil, o iddiaların hangi sınıf yoluna karşı koştuğuna da bağlıdır.
Özet
- Sahte bağımlılık ailesi M21/K02’de kuruldu; bu derste eklenen tek soru sahte tipin hangi sınıf yolunda durduğudur.
- Aynı derlenmiş test, aynı adı taşıyan gerçek ve sahte gerçekleştirimlere karşı yeniden derlenmeden koşturuldu ve iki ayrı sonuç verdi; ikisi de hatasız tamamlandı.
- Sahte gerçekleştirimin tuttuğu çağrı sayacı, casus nesnenin sinyalini üretim gerçekleştiriminde bulunmayan bir alan üzerinden taşıdı.
- İmzası değişen bir gerçekleştirimle koşum
NoSuchMethodExceptionverdi; bu istisna koşumda düştü ve nedeni test metninde görünmedi. - Aynı adı taşıyan iki tip yalnız imzaları uyuştuğu sürece birbirinin yerine geçebilir; imza ayrıldığında kusur derlemede değil koşumda ortaya çıkar.
Sonraki Adım
Bu derste tek bir test gövdesi üç ayrı sınıf yoluna karşı koşturuldu, ama her koşumda aynı üç sayı — 5, 3, 8 — kullanıldı. Bir test gövdesini birden çok veri kümesiyle çoğaltmak ayrı bir soru açıyor: aynı gövde, aynı iddia, farklı girdiler — kaç ayrı koşum, kaç ayrı başarısızlık, ve başarısızlığın hangi veri satırından geldiği nasıl kayda geçiyor? Sıradaki ders bunu tek bir gövdeyi bir veri kümesi üzerinde tekrar tekrar çalıştırarak sayıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.