---
title: Sahteleme
source: 'https://academia.sh/tr/kurslar/java-ekosistem/sahteleme'
course: 'Derleme, Test ve Ekosistem'
language: tr
updated: '2026-08-23T14:25:06+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

Ö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ğı `Tedarikci` adı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.

```java
// 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 kendi
  `URLClassLoader`'ı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 `NoSuchMethodException` verdi; 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.
