İçeriğe geç
academia.sh

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 Envanter adlı, 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ş .class dosyaları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 — 30 ve 150 — 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 InvocationTargetException iç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 IllegalStateException fı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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat