İçeriğe geç
academia.sh

Ders 03 / 10

Test Çerçeveleri

assert() tabanlı bir sınamanın hangi profilde tutmayan iddiayı yakaladığını, hangi profilde her şeyi geçirdiğini ve kendi karşılaştırma yardımcısının neden iki yanda da çalıştığını ölçün.

Yayın Güncelleme

Bu derste

  • Birim, tümleşim ve davranış testi arasındaki farkı ve test çerçevesinin ne sağladığını tanımlar
  • assert() tabanlı bir sınamanın geliştirme ve üretim profilinde nasıl davrandığını ölçer
  • İddia içindeki durumu değiştiren çağrının üretim profilinde koşmadığını gösterir
  • zend.assertions için 0 ve -1 değerlerinin farkını ölçer
İçindekiler

Önceki ders, kodun biçiminin koşumu değiştirmediğini ve bu yüzden stil denetiminin koşumun dışında durduğunu gösterdi. Test koşumun dışında mı durur, içinde mi? Bu sorunun cevabı, testin neye dayandığına bağlıdır. PHP’de en küçük sınama aracı assert() fonksiyonudur ve onun davranışı, iki ayar dosyasının yedi farkından birine bağlıdır: zend.assertions.

Üç tür test, sınanan şeyin büyüklüğüne göre ayrılır. Birim testi (unit test) tek bir fonksiyonu ya da sınıfı yalıtılmış olarak sınar. Tümleşim testi (integration test) birkaç parçanın birlikte çalışmasını, örneğin bir fonksiyonun bir dosya ya da veritabanıyla birlikte davranışını sınar. Davranış testi (behavior test) sistemi dışarıdan, kullanıcının gördüğü sonuç üzerinden sınar. Test çerçevesi (test framework) bu testleri yazmak ve koşturmak için ortak bir iskelet sağlar: testleri bulur, her birini ayrı koşturur, sonuçları toplar ve geçen ile kalanı raporlar. Bu ders hiçbir test çerçevesinin adını anmaz ve onun yerine test çerçevelerinin dayandığı mekanizmayı ölçer: iddia (assertion).

Rust müfredatının beşinci kursunda (M10/K05) güvencenin nerede bittiği arandı. Burada güvencenin sınırı değil, iddianın hangi profilde derlendiği ölçülür.

assert() ile Sınama

Ölçüm çekirdeği önceki derslerle aynıdır. yan.php hiç değişmez.

<?php
// yan.php — bu davranış üretime çıkıyor mu?
declare(strict_types=1);

// PHP'nin dağıttığı iki ayar dosyası (php.ini-development, php.ini-production) yedi
// yönergede ayrışır; yedisi buraya birebir alındı. Üretimdeki son iki satır dağıtım
// dosyasında yorum satırıdır: kod önbelleği komut satırında da açık, dosya denetimi kapalı.
const PROFILLER = [
    'geliştirme' => [
        'display_errors' => '1',
        'display_startup_errors' => '1',
        'error_reporting' => '32767',
        'mysqlnd.collect_memory_statistics' => '1',
        'zend.assertions' => '1',
        'zend.exception_ignore_args' => '0',
        'zend.exception_string_param_max_len' => '15',
    ],
    'üretim' => [
        'display_errors' => '0',
        'display_startup_errors' => '0',
        'error_reporting' => '24575',
        'mysqlnd.collect_memory_statistics' => '0',
        'zend.assertions' => '-1',
        'zend.exception_ignore_args' => '1',
        'zend.exception_string_param_max_len' => '0',
        'opcache.enable_cli' => '1',
        'opcache.validate_timestamps' => '0',
    ],
];

/** Betiği ayar dosyası olmadan, yalnız profilin yönergeleriyle ayrı bir süreçte koşturur. */
function calistir(string $betik, array $yonergeler): bool {
    $komut = ['php', '-n'];
    foreach ($yonergeler as $ad => $deger) {
        array_push($komut, '-d', "$ad=$deger");
    }
    $komut[] = $betik;
    $surec = proc_open($komut, [1 => ['pipe', 'w'], 2 => ['file', '/dev/null', 'w']], $borular);
    $cikti = stream_get_contents($borular[1]);
    fclose($borular[1]);
    proc_close($surec);
    $satirlar = array_values(array_filter(array_map('trim', explode("\n", $cikti))));
    return end($satirlar) === 'evet';
}

/** Davranışın iki koşumda görülüp görülmediğinden yanını çıkarır. */
function yan(bool $gelistirmede, bool $uretimde): string {
    return match (true) {
        $gelistirmede && $uretimde => 'iki yanda da',
        $gelistirmede => 'yalnız geliştirmede',
        $uretimde => 'yalnız üretimde',
        default => 'koşuma girmiyor',
    };
}

function yanRaporu(string $ad, string $betik): string {
    $gelistirmede = calistir($betik, PROFILLER['geliştirme']);
    $uretimde = calistir($betik, PROFILLER['üretim']);
    $sonYan = yan($gelistirmede, $uretimde);
    echo "$ad → $sonYan\n    geliştirme ", $gelistirmede ? 'evet' : 'hayır',
        ' · üretim ', $uretimde ? 'evet' : 'hayır', "\n";
    return $sonYan;
}

Sınanan kod, bilerek yanlış yazılmış bir toplama fonksiyonudur: topla() iki sayıyı toplamak yerine çıkarır. iddiaYakalandi() fonksiyonu assert(topla(2, 3) === 5) iddiasını bir try bloğunda koşturur ve AssertionError fırlatılırsa true döndürür. say() fonksiyonu ise her çağrıldığında sayacı artırır: durumu değiştiren bir çağrının, bir iddianın içinde nasıl davrandığını ölçmek için vardır. Üçüncü yardımcı, esitOlmali(), assert() kullanmaz; iki değeri katı eşitlik karşılaştırmasıyla kıyaslar ve ayrışırlarsa RuntimeException fırlatır.

<?php
// sinama.php — bilerek yanlış fonksiyon ve üç sınama yardımcısı
declare(strict_types=1);

function topla(int $a, int $b): int {
    return $a - $b;
}

function iddiaYakalandi(): bool {
    try {
        assert(topla(2, 3) === 5);
        return false;
    } catch (AssertionError) {
        return true;
    }
}

$sayac = 0;

function say(): bool {
    global $sayac;
    $sayac++;
    return true;
}

function esitOlmali(mixed $beklenen, mixed $gelen): void {
    if ($beklenen !== $gelen) {
        throw new RuntimeException('beklenen ile gelen ayrıştı');
    }
}

Sürücü üç yoklamayı iki profilde koşturur. Birincisi tutmayan iddianın yakalanıp yakalanmadığını, ikincisi iddia içindeki çağrının koşup koşmadığını, üçüncüsü esitOlmali() ile yazılmış aynı sınamanın yanlışı yakalayıp yakalamadığını sorar.

<?php
// sinamalar.php — üç sınama, iki profil
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'tutmayan iddia yakalanır' => <<<'PHP'
        <?php
        require 'sinama.php';
        echo iddiaYakalandi() ? "evet\n" : "hayır\n";
        PHP,
    'iddia içindeki çağrı koşar' => <<<'PHP'
        <?php
        require 'sinama.php';
        assert(say());
        echo $sayac === 1 ? "evet\n" : "hayır\n";
        PHP,
    'kendi karşılaştırma yardımcısı yanlışı yakalar' => <<<'PHP'
        <?php
        require 'sinama.php';
        try {
            esitOlmali(5, topla(2, 3));
            echo "hayır\n";
        } catch (RuntimeException) {
            echo "evet\n";
        }
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
tutmayan iddia yakalanır → yalnız geliştirmede
    geliştirme evet · üretim hayır
iddia içindeki çağrı koşar → yalnız geliştirmede
    geliştirme evet · üretim hayır
kendi karşılaştırma yardımcısı yanlışı yakalar → iki yanda da
    geliştirme evet · üretim evet

İlk iki yoklama yalnız geliştirmede çıkar. Geliştirme profilinde (zend.assertions=1) tutmayan iddia AssertionError fırlatır ve sınama kalır. Üretim profilinde (zend.assertions=-1) PHP assert() çağrısını hiç derlemez; iddia ne bir değer üretir ne bir istisna fırlatır. Aynı yanlış fonksiyon üretim profilinde sınamadan geçer. Bir testin geçmesi, kodun doğru olduğunu söylemez; testin hangi profilde koştuğunu da söyler.

İkinci yoklama bunun ayrıntısını gösterir. İddia derlenmediğinde içindeki ifade de değerlendirilmez. assert(say()) içindeki çağrı, geliştirmede sayacı artırır, üretimde artırmaz. Aşağıdaki betik iki değeri doğrudan basar.

# sayac.sh — iddia içindeki çağrı hangi profilde koşar
cat > sayac.php <<'PHP'
<?php
require 'sinama.php';
assert(say());
echo "sayaç: $sayac\n";
PHP
echo "zend.assertions=1:"
php -n -d zend.assertions=1 sayac.php
echo "zend.assertions=-1:"
php -n -d zend.assertions=-1 sayac.php
zend.assertions=1:
sayaç: 1
zend.assertions=-1:
sayaç: 0

Bu, iddiayı bir sınama aracı olarak kullanmanın bilinen tuzağıdır: iddianın içine bir işi yaptıran çağrı konursa iş yalnız bir profilde yapılır. İddia içindeki ifade yalnızca bir koşulu okumalı, durumu değiştirmemelidir.

Kendi Karşılaştırma Yardımcısı

Üçüncü yoklama farklı bir sonuç verir: esitOlmali() iki profilde de yanlışı yakalar. Yardımcı assert() çağırmaz ve AssertionError’a dayanmaz; kendi koşulunu kendisi değerlendirir ve kendi istisnasını kendisi fırlatır. Bu yüzden zend.assertions ayarı onu etkilemez. Test çerçevelerinin karşılaştırma yöntemleri de aynı sebeple assert() üzerine kurulmaz: bir testin üretim profilinde sessizce geçmesi, o testin hiçbir şey sınamadığı anlamına gelir.

Bunun bedeli küçüktür: iki satırlık bir yardımcı, assert() ile aynı işi yapar ve profilden bağımsızdır. Geriye bir ayrım kalır. assert() kodun içine yazılan, çalışma sırasında bir varsayımı sorgulayan bir iddiadır; geliştirmede fazladan güvence sağlar ve üretimde bedeli sıfıra indirilir. Test ise koşumun dışında, kodu çağıran ayrı bir programdır ve her profilde aynı sonucu üretmelidir.

Test Çerçevesinin Yaptığı İş

Bu dersin sürücüsü küçük bir test çerçevesidir. Yoklamaları bir listede toplar, her birini ayrı bir dosyaya yazar, her dosyayı ayrı bir süreçte koşturur ve sonucu tek satırda raporlar. Üç iş, gerçek test çerçevelerinin de çekirdeğidir. Testleri bulma, listeyi kurar. Yalıtma, bir testin bıraktığı durumun öbürünü etkilemesini engeller: ikinci yoklamadaki sayaç, birincideki sayaçtan bağımsızdır, çünkü her yoklama yeni bir süreçtir. Raporlama, geçen ile kalanı tek yerde toplar.

Yalıtmanın bedeli vardır. Ayrı süreç, testin gördüğü ayarları da ayrı kılar. Bu derste her yoklama, ayar dosyası olmadan yalnız profilin yönergeleriyle koşar; çünkü bir testin hangi ayarla koştuğu, sonucunu belirler. Aynı test paketi geliştirme ayarıyla geçip üretim ayarıyla kalabilir ya da tersi olabilir. Birinci yoklama tam olarak bunu gösterir: iddiaya dayanan bir sınama yalnız zend.assertions=1 olduğunda yanlışı bulur.

Pratikte iki sonuç çıkar. Testler, üretime çıkacak yapılandırmaya yakın bir profilde de koşturulmalıdır; yoksa üretimde değişen bir davranış, hiç sınanmadan canlıya geçer. Öte yandan iddiaların kendisi geliştirmede koşmaya devam etmelidir, çünkü onların işi kodun içinde varsayımı denetlemektir. İki işi birbirinden ayırmak, yani sınamayı profilden bağımsız bir karşılaştırma yardımcısına, varsayımı ise assert()’e bırakmak, bu yüzden tutarlı bir ayrımdır.

Birim ve Tümleşim Sınaması

Başa dönüp birim testi ile tümleşim testi arasındaki farkı küçük bir örnekle ölçelim. Saf bir fonksiyon, kdvli(), yalnız aldığı değere bağlıdır: aynı girdi her koşumda aynı çıktıyı verir. Dosyaya yazan bir fonksiyon, notEkle(), ise dışarıdaki bir duruma bağlıdır: önceki koşumdan kalan bir dosya, sonucu değiştirir.

Sürücü dört yoklama koşturur. Birincisi saf fonksiyonu sınar. İkincisi dosyaya yazan fonksiyonu, dosyayı temizlemeden sınar. Üçüncüsü aynı sınamayı, başta dosyayı silerek yazar. Dördüncüsü, başarısızlık iletisinin iki profilde aynı olup olmadığını sorar. Sürücü, her yoklamadan önce dosyayı siler; yoklamanın içindeki iki koşum (geliştirme, sonra üretim) ise aynı dosyayı paylaşır.

<?php
// birim.php — saf fonksiyon ile dosyaya yazan fonksiyon
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'birim: saf fonksiyon' => <<<'PHP'
        <?php
        require 'sinama.php';
        function kdvli(int $tutar): int {
            return intdiv($tutar * 118, 100);
        }
        try {
            esitOlmali(118, kdvli(100));
            echo "evet\n";
        } catch (RuntimeException) {
            echo "hayır\n";
        }
        PHP,
    'tümleşim: dosyayı temizlemeyen sınama' => <<<'PHP'
        <?php
        function notEkle(string $dosya, string $satir): void {
            file_put_contents($dosya, $satir . "\n", FILE_APPEND);
        }
        notEkle('not.txt', 'ilk');
        echo count(file('not.txt')) === 1 ? "evet\n" : "hayır\n";
        PHP,
    'tümleşim: başta temizleyen sınama' => <<<'PHP'
        <?php
        function notEkle(string $dosya, string $satir): void {
            file_put_contents($dosya, $satir . "\n", FILE_APPEND);
        }
        @unlink('not.txt');
        notEkle('not.txt', 'ilk');
        echo count(file('not.txt')) === 1 ? "evet\n" : "hayır\n";
        PHP,
    'başarısızlık iletisi aynıdır' => <<<'PHP'
        <?php
        require 'sinama.php';
        try {
            esitOlmali(5, topla(2, 3));
            echo "hayır\n";
        } catch (RuntimeException $e) {
            echo $e->getMessage() === 'beklenen ile gelen ayrıştı' ? "evet\n" : "hayır\n";
        }
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    @unlink('not.txt');
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
birim: saf fonksiyon → iki yanda da
    geliştirme evet · üretim evet
tümleşim: dosyayı temizlemeyen sınama → yalnız geliştirmede
    geliştirme evet · üretim hayır
tümleşim: başta temizleyen sınama → iki yanda da
    geliştirme evet · üretim evet
başarısızlık iletisi aynıdır → iki yanda da
    geliştirme evet · üretim evet

Saf fonksiyonun sınaması iki yanda da geçer: girdisi ve çıktısı dışında hiçbir şeye bağlı değildir. Temizlemeyen tümleşim sınaması ise yalnız geliştirmede geçer. Bu bir profil farkı değildir; sürücünün koşum sırasının izidir. Geliştirme koşumu dosyayı ilk kez oluşturur ve tek satır görür; üretim koşumu aynı dosyaya ikinci satırı ekler ve iki satır görür. Koşum sırası tersine çevrilse sonuç da tersine dönerdi. Başta temizleyen sınama bu yüzden iki yanda da geçer.

Bu, tümleşim testlerinin tanımını da verir: dışarıdaki durumu kullanan test, o durumu kendi başına kurmalı ve temizlemelidir. Yoksa test sonucu, kodun doğruluğunu değil, testin hangi sırayla koştuğunu ölçer. Sonuç, yalıtmanın neden bir test çerçevesinin temel işi olduğunu gösterir. Dördüncü yoklama ise bir başka ayrıma bakar: esitOlmali() başarısızlık iletisini ayardan bağımsız üretir, bu yüzden test raporu iki profilde aynı metni taşır ve bir kalanın sebebi profile bakılmadan okunabilir.

Sınırlayıcı Ölçüm

zend.assertions üç değer alır. 1 iddiayı derler ve koşturur; -1 derlemez; 0 ise derler ama koşturmaz. Ders boyunca üretim profili -1 kullandı. Sorulacak soru şudur: 0 değeri de iddiayı susturur mu, ve iki değer arasında davranış farkı var mı? Çekirdek profil değiştirilmez; sürücü yan.php’deki calistir() fonksiyonunu üretim profilinin kopyasıyla çağırır ve yalnız zend.assertions değerini değiştirir.

<?php
// sinir.php — zend.assertions için 0 ile -1 farkı
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'iddia yakalanır' => <<<'PHP'
        <?php
        require 'sinama.php';
        echo iddiaYakalandi() ? "evet\n" : "hayır\n";
        PHP,
    'çalışırken açılınca iddia yakalanır' => <<<'PHP'
        <?php
        require 'sinama.php';
        ini_set('zend.assertions', '1');
        echo iddiaYakalandi() ? "evet\n" : "hayır\n";
        PHP,
];

foreach (['0', '-1'] as $deger) {
    $profil = ['zend.assertions' => $deger] + PROFILLER['üretim'];
    foreach ($yoklamalar as $ad => $kod) {
        file_put_contents('yoklama.php', $kod);
        echo "zend.assertions=$deger · $ad: ", calistir('yoklama.php', $profil) ? 'evet' : 'hayır', "\n";
    }
}
zend.assertions=0 · iddia yakalanır: hayır
zend.assertions=0 · çalışırken açılınca iddia yakalanır: evet
zend.assertions=-1 · iddia yakalanır: hayır
zend.assertions=-1 · çalışırken açılınca iddia yakalanır: hayır

0 değerinde de tutmayan iddia yakalanmaz: iddia derlenmiş olsa bile koşturulmaz. İki değer arasındaki fark, çalışırken ortaya çıkar. 0 değerinde ayar ini_set() ile 1’e çevrilebilir ve iddia o andan sonra yakalar; -1 değerinde iddia hiç derlenmediği için aynı çağrı bir şey değiştirmez. Bu, iddiayı açıp kapatan sınırın yalnız ayarın değerinde değil, dosyanın derlendiği anda geçerli ayarda da yattığını gösterir. Üretim profilinin -1 seçmesi bu yüzden bir sertleştirme (hardening) kararıdır: iddiayı bir istek içinde yeniden açmanın yolu kapalıdır.

Özet

  • Test çerçevesi, testleri bulan, koşturan ve raporlayan iskelettir; birim, tümleşim ve davranış testi sınanan şeyin büyüklüğüne göre ayrılır.
  • assert() tabanlı sınama yalnız geliştirmede tutmayan iddiayı yakalar; üretim profilinde (zend.assertions=-1) iddia derlenmez ve yanlış kod sınamadan geçer.
  • İddia içindeki durumu değiştiren çağrı üretimde koşmaz: sayaç geliştirmede 1, üretimde 0 kalır.
  • AssertionError’a dayanmayan kendi karşılaştırma yardımcısı iki yanda da çalışır; test, bu yüzden assert()’e değil böyle bir yardımcıya dayanır.
  • zend.assertions=0 iddiayı derler ama koşturmaz; çalışırken 1’e çevrilebilir. -1’de bu yol kapalıdır.

Sonraki Adım

Bu derste bir iddiayı geliştirmede görünür kılan ayar, üretimde onu sessizce kaldırdı. Aynı yön, hata ayıklarken gördüğümüz bilgi için de geçerlidir: izdeki argüman değerleri, ekrana basılan uyarı ve bir hata ayıklayıcının durduğu satırdaki değişkenler. Sonraki ders, geliştirmede görülen bu bilginin üretime çıkıp çıkmadığını sorar ve bir değerin istisna izinde göründüğü ya da gizlendiği yeri ölçer.

İ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