D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

birim testi: [unit test], programın alt birimlerinin bağımsız olarak denetlenmeleri
blok: [block], küme parantezleriyle gruplanmış ifadelerin tümü
derleyici: [compiler], programlama dili kodunu bilgisayarın anladığı makine koduna çeviren program
iç olanak: [core feature], dilin kütüphane gerektirmeyen bir olanağı
ifade: [expression], programın değer oluşturan veya yan etki üreten bir bölümü
kütüphane: [library], belirli bir konuda çözüm getiren tür tanımlarının ve işlevlerin bir araya gelmesi
... bütün sözlük



İngilizce Kaynaklar


Diğer




Birim Testleri

Programcılığın kaçınılmaz uğraşlarından birisi hata ayıklamaktır.

Her kullanıcının yakından tanıdığı gibi, içinde bilgisayar programı çalışan her cihaz yazılım hataları içerir. Yazılım hataları, kol saati gibi basit elektronik aletlerden uzay aracı gibi büyük sistemlere kadar her yerde bulunur.

Hata nedenleri

Yazılım hatalarının çok çeşitli nedenleri vardır. Programın fikir aşamasından başlayarak kodlanmasına doğru kabaca sıralarsak:

Ne yazık ki, günümüzde henüz tam olarak sağlam kod üreten yazılım geliştirme yöntemleri bulunamamıştır. Bu konu, sürekli olarak çözüm bulunmaya çalışılan ve her beş on yılda bir ümit verici yöntemlerin ortaya çıktığı bir konudur.

Hatanın farkedildiği zaman

Yazılım hatasının ne zaman farkına varıldığı da çeşitlilik gösterir. En erkenden en geçe doğru sıralayarak:

Hata ne kadar erken farkedilirse hem zararı o kadar az olur, hem de o kadar az sayıda insanın zamanını almış olur. Bu yüzden en iyisi, hatanın kodun yazıldığı sırada yakalanmasıdır. Geç farkedilen hata ise başka programcıların, programı test edenlerin, ve çok sayıdaki kullanıcının da zamanını alır.

Son kullanıcıya gidene kadar farkedilmemiş olan bir hatanın kodun hangi noktasından kaynaklandığını bulmak da çoğu durumda oldukça zordur. Bu noktaya kadar farkedilmemiş olan bir hata, bazen aylarca sürebilen uğraşlar sonucunda temizlenebilir.

Hata yakalamada birim testleri

Kodu yazan programcı olmazsa zaten kod olmaz. Ayrıca, derlemeli bir dil olduğu için D programları zaten derleyici kullanmadan oluşturulamazlar. Bunları bir kenara bıraktığımızda, program hatalarını yakalamada en erken ve bu yüzden de en etkin yöntem olarak birim testleri kalır.

Birim testleri, modern programcılığın ayrılmaz araçlarındandır. Kod hatalarını azaltma konusunda en etkili yöntemlerdendir. Birim testleri olmayan kod, hatalı kod olarak kabul edilir.

Ne yazık ki bunun tersi doğru değildir: birim testlerinin olması, kodun hatasız olduğunu kanıtlamaz; ama hata oranını çok büyük ölçüde azaltır.

Birim testleri ayrıca kodun rahatça ve güvenle geliştirilebilmesini de sağlarlar. Kod üzerinde değişiklik yapmak, örneğin yeni olanaklar eklemek, doğal olarak o kodun eski olanaklarının artık hatalı hale gelmelerine neden olabilir. Kodun geliştirilmesi sırasında ortaya çıkan böyle hatalar, ya çok sonraki sürüm testleri sırasında farkedilirler, ya da daha kötüsü, program son kullanıcılar tarafından kullanılırken.

Bu tür hatalar kodun yeniden düzenlenmesinden çekinilmesine ve kodun gittikçe çürümesine (code rot) neden olurlar. Örneğin bazı satırların aslında yeni bir işlev olarak yazılmasının gerektiği bir durumda, yeni hatalardan korkulduğu için koda dokunulmaz ve kod tekrarı gibi zararlı durumlara düşülebilir.

Programcı kültüründe duyulan "bozuk değilse düzeltme" ("if it isn't broken, don't fix it") gibi sözler, hep bu korkunun ürünüdür. Bu gibi sözler, yazılmış olan koda dokunmamayı erdem olarak gösterdikleri için zaman geçtikçe kodun çürümesine ve üzerinde değişiklik yapılamaz hale gelmesine neden olurlar.

Modern programcılıkta bu düşüncelerin yeri yoktur. Tam tersine, kod çürümesinin önüne geçmek için kodun gerektikçe serbestçe geliştirilmesi önerilir: "acımasızca geliştir" ("refactor mercilessly"). İşte bu yararlı yaklaşımın en güçlü silahı birim testleridir.

Birim testi, programı oluşturan en alt birimlerin birbirlerinden olabildiğince bağımsız olarak test edilmeleri anlamına gelir. Alt birimlerin bağımsız olarak testlerden geçmeleri, o birimlerin birlikte çalışmaları sırasında oluşacak hataların olasılığını büyük ölçüde azaltır. Eğer parçalar doğru çalışıyorsa, bütünün de doğru çalışma olasılığı artar.

Birim testleri başka bazı dillerde JUnit, CppUnit, Unittest++, vs. gibi kütüphane olanakları olarak gerçekleştirilmişlerdir. D'de ise birim testleri dilin iç olanakları arasındadır. Her iki yaklaşımın da üstün olduğu yanlar gösterilebilir. D birim testleri konusunda bazı kütüphanelerin sunduğu bazı olanakları içermez. Bu yüzden birim testleri için ayrı bir kütüphaneden yararlanmak da düşünülebilir.

D'de birim testleri, önceki bölümde gördüğümüz assert denetimlerinin unittest blokları içinde kullanılmalarından oluşurlar. Ben burada yalnızca D'nin bu iç olanağını göstereceğim.

Birim testlerini başlatmak

Programın asıl işleyişi ile ilgili olmadıkları için, birim testlerinin yalnızca programın geliştirilmesi aşamasında çalıştırılmaları gerekir. Birim testleri derleyici veya geliştirme ortamı tarafından, ve ancak özellikle istendiğinde başlatılır.

Birim testlerinin nasıl başlatıldıkları kullanılan derleyiciye ve geliştirme ortamına göre değişir. Ben burada örnek olarak Digital Mars'ın derleyicisi olan dmd'nin ‑unittest seçeneğini göstereceğim.

Programın deneme.d isimli bir kaynak dosyaya yazıldığını varsayarsak komut satırına ‑unittest seçeneğini eklemek birim testlerini etkinleştirmek için yeterlidir:

dmd deneme.d -w -unittest

Bu şekilde oluşturulan program çalıştırıldığında önce birim testleri işletilir ve ancak onlar başarıyla tamamlanmışsa programın işleyişi main ile devam eder.

unittest blokları

Birim testlerini oluşturan kodlar bu blokların içine yazılır. Bu kodların programın normal işleyişi ile ilgileri yoktur; yalnızca programı ve özellikle işlevleri denemek için kullanılırlar:

unittest {
    /* ... birim testleri ve testler için gereken kodlar ... */
}

unittest bloklarını sanki işlev tanımlıyor gibi kendi başlarına yazabilirsiniz. Ama daha iyisi, bu blokları denetledikleri işlevlerin hemen altına yazmaktır.

Örnek olarak, bir önceki bölümde gördüğümüz ve kendisine verilen sayıya Türkçe ses uyumuna uygun olarak da eki döndüren işleve bakalım. Bu işlevin doğru çalışmasını denetlemek için, unittest bloğuna bu işlevin döndürmesini beklediğimiz koşullar yazarız:

dstring daEki(int sayı) {
    // ...
}

unittest {
    assert(daEki(1) == "de");
    assert(daEki(5) == "te");
    assert(daEki(9) == "da");
}

Oradaki üç koşul; 1, 5, ve 9 sayıları için sırasıyla "de", "te", ve "da" döndürüldüğünü denetler.

Her ne kadar testlerin temeli assert denetimleri olsa da, unittest bloklarının içinde her türlü D olanağını kullanabilirsiniz. Örneğin, bir dizgi içindeki belirli bir harfi o dizginin en başında olacak şekilde döndüren bir işlevin testleri şöyle yazılabilir:

dstring harfBaşa(dstring dizgi, dchar harf) {
    // ...
}

unittest {
    immutable dizgi = "merhaba"d;

    assert(harfBaşa(dizgi, 'm') == "merhaba");
    assert(harfBaşa(dizgi, 'e') == "emrhaba");
    assert(harfBaşa(dizgi, 'a') == "aamerhb");
}

Oradaki üç assert denetimi harfBaşa işlevinin nasıl çalışmasının beklendiğini denetliyorlar.

Bu örneklerde görüldüğü gibi, birim testleri aynı zamanda işlevlerin belgeleri ve örnek kodları olarak da kullanışlıdırlar. Yalnızca birim testine bakarak işlevin kullanılışı hakkında hızlıca fikir edinebiliriz.

Hata atılıp atılmadığının denetlenmesi

Kodun belirli durumlar karşısında hata atıp atmadığının da denetlenmesi gerekebilir. std.exception modülü bu konuda yardımcı olan iki işlev içerir:

Örneğin, iki dilim parametresinin eşit uzunlukta olduğunu şart koşan ve boş dilimlerle de hatasız çalışması gereken bir işlev aşağıdaki gibi denetlenebilir:

import std.exception;

int[] ortalama(int[] a, int[] b) {
    // ...
}

unittest {
    /* Eşit uzunluklu olmayan dilimlerde hata atılmalıdır */
    assertThrown(ortalama([1], [1, 2]));

    /* Boş dilimlerde hata atılmamalıdır */
    assertNotThrown(ortalama([], []));
}

assertThrown normalde türüne bakmaksızın herhangi bir hatanın atıldığını denetler; gerektiğinde özel bir hata türünün atıldığını da denetleyebilir. Benzer biçimde, assertNotThrown da normalde hiçbir hatanın atılmadığını denetler ama gerektiğinde o da belirli bir hata türünün atılmadığını denetleyebilir. Özel hata türü bu işlevlere şablon parametresi olarak bildirilir:

    /* Eşit uzunluklu olmayan dilimlerde UzunlukHatası
     * atılmalıdır */
    assertThrown!UzunlukHatası(ortalama([1], [1, 2]));

    /* Boş dilimlerde RangeError atılmamalıdır (yine de başka
     * türden hata atılabilir) */
    assertNotThrown!RangeError(ortalama([], []));

Şablonları ilerideki bir bölümde göreceğiz.

Bu işlevlerin temel amacı kodu kısaltmak ve okunurluğu arttırmaktır. Yoksa, aşağıdaki assertThrown satırı aslında hemen altındaki uzun kodun eşdeğeridir:

    assertThrown(ortalama([1], [1, 2]));

// ...

    /* Yukarıdaki satırın eşdeğeri */
    {
        auto atıldı_mı = false;

        try {
            ortalama([1], [1, 2]);

        } catch (Exception hata) {
            atıldı_mı = true;
        }

        assert(atıldı_mı);
    }
Test yönelimli programlama: önce test, sonra kod

Modern programcılık yöntemlerinden olan test yönelimli programlama ("test driven development" - TDD), birim testlerinin kod yazılmadan önce yazılmasını öngörür. Bu yöntemde asıl olan birim testleridir. Kodun yazılması, birim testlerinin başarıya ulaşmalarını sağlayan ikincil bir uğraştır.

Yukarıdaki daEki işlevine bu bakış açısıyla yaklaşarak onu önce birim testleriyle şöyle yazmamız gerekir:

dstring daEki(int sayı) {
    return "bilerek hatalı";
}

unittest {
    assert(daEki(1) == "de");
    assert(daEki(5) == "te");
    assert(daEki(9) == "da");
}

void main() {
}

Her ne kadar o işlevin hatalı olduğu açık olsa da, önce programın birim testlerinin doğru olarak çalıştıklarını, yani beklendiği gibi hata attıklarını görmek isteriz:

$ dmd deneme.d -w -O -unittest
$ ./deneme 
core.exception.AssertError@deneme(8): unittest failure

İşlev ancak ondan sonra ve bu testleri geçecek şekilde yazılır:

dstring daEki(int sayı) {
    dstring ek;

    immutable sonHane = sayı % 10;

    final switch (sonHane) {

    case 1:
    case 2:
    case 7:
    case 8:
        ek = "de";
        break;

    case 3:
    case 4:
    case 5:
        ek = "te";
        break;

    case 6:
    case 9:
    case 0:
        ek = "da";
        break;
    }

    return ek;
}

unittest {
    assert(daEki(1) == "de");
    assert(daEki(5) == "te");
    assert(daEki(9) == "da");
}

void main() {
}

Artık program bu testleri geçer, ve bizim de daEki işlevi konusunda güvenimiz gelişir. Bu işlevde daha sonradan yapılacak olası geliştirmeler, unittest bloğuna yazdığımız koşulları korumak zorundadırlar. Böylelikle kodu geliştirmeye güvenle devam edebiliriz.

Bazen de önce hata, sonra test, ve en sonunda kod

Birim testleri bütün durumları kapsayamazlar. Örneğin yukarıdaki testlerde üç farklı eki üreten üç sayı değeri seçilmiş, ve daEki işlevi bu üç testten geçtiği için başarılı kabul edilmiştir.

Bu yüzden, her ne kadar çok etkili yöntemler olsalar da, birim testleri bütün hataları yakalayamazlar ve bazı hatalar bazen son kullanıcılara kadar saklı kalabilir.

daEki işlevi için bunun örneğini assert bölümünün problemlerinde de görmüştük. O problemde olduğu gibi, bu işlev 50 gibi bir değer geldiğinde hatalıdır:

import std.stdio;

void main() {
    writefln("%s'%s", 50, daEki(50));
}

Çıktısı:

$ ./deneme
50'da

İşlev yalnızca son haneye baktığı için 50 için "de" yerine hatalı olarak "da" döndürmektedir.

Test yönelimli programlama işlevi hemen düzeltmek yerine öncelikle bu hatalı durumu yakalayan bir birim testinin eklenmesini önerir. Çünkü hatanın birim testlerinin gözünden kaçarak programın kullanımı sırasında ortaya çıkmış olması, birim testlerinin bir yetersizliği olarak görülür. Buna uygun olarak bu durumu yakalayan bir test örneğin şöyle yazılabilir:

unittest {
    assert(daEki(1) == "de");
    assert(daEki(5) == "te");
    assert(daEki(9) == "da");
    assert(daEki(50) == "de");
}

Program bu sefer bu birim testi denetimi nedeniyle sonlanır:

$ ./deneme 
core.exception.AssertError@deneme(39): unittest failure

Artık bu hatalı durumu denetleyen bir test bulunduğu için, işlevde ileride yapılabilecek geliştirmelerin tekrardan böyle bir hataya neden olmasının önüne geçilmiş olur.

Kod ancak bu birim testi yazıldıktan sonra, ve o testi geçirmek için yazılır.

Not: Bu işlev, sonu "bin" ve "milyon" gibi okunarak biten başka sayılarla da sorunlu olduğu için burada kapsamlı bir çözüm bulmaya çalışmayacağım.

Problem

Yukarıda sözü geçen harfBaşa işlevini, birim testlerini geçecek şekilde gerçekleştirin:

dstring harfBaşa(dstring dizgi, dchar harf) {
    dstring sonuç;
    return sonuç;
}

unittest {
    dstring dizgi = "merhaba"d;

    assert(harfBaşa(dizgi, 'm') == "merhaba");
    assert(harfBaşa(dizgi, 'e') == "emrhaba");
    assert(harfBaşa(dizgi, 'a') == "aamerhb");
}

void main() {
}

O tanımdan başlayın; ilk test yüzünden hata atıldığını görün; ve işlevi hatayı giderecek şekilde yazın.