D.ershane D Programlama Dili Dersleri

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

Bölümler
İ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. Bunun bir örneği olarak, D'de birim testleri konusunu kütüphane olarak halleden Dunit projesine de bakmak isteyebilirsiniz.

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

Bu blokları anlatmadan önce, birim testlerinin nasıl başlatıldıklarını göstermem gerekiyor.

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ında normalde şu şekilde oluşturabiliriz:

dmd deneme.d -ofdeneme -w

Kısaca: dmd, derleyici; deneme.d, derlenmesi istenen kaynak dosya; -of, oluşturulacak olan programın ismini belirleyen seçenek (bu durumda deneme); ve -w da derleyici uyarılarını etkinleştiren seçenektir.

Programı kodun içindeki birim testlerini de ekleyerek oluşturmak için bu komut satırına -unittest seçeneği eklenir:

dmd deneme.d -ofdeneme -w -unittest

Bu şekilde oluşturulan program çalıştırıldığında önce birim testleri işletilir ve ancak onlar başarıyla tamamlanırsa 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
{
    işlevi_deneyen_kodlar_ve_assert_ifadeleri
}

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 derste 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(in 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:

dchar[] harfBaşa(in dchar[] dizgi, in dchar harf)
{
    // ...
}
unittest
{
    dchar[] dizgi = "merhaba"d.dup;

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

Oradaki üç assert ifadesi, 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.

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(in 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 -ofdeneme -w -O -unittest
$ ./deneme 
core.exception.AssertError@deneme.d(7): Assertion failure

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

dstring daEki(in int sayı)
{
    dstring ek;

    const int sonHane = sayı % 10;

    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;

    default:
        // Buraya hiçbir durumda gelmemeliyiz
        assert(false);
    }

    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 dersinin 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));
}
$ ./deneme
50'da

"50'de" olması gerektiği halde, işlev yalnızca son haneye baktığı için bu durumda 50 için hatalı olarak "da" döndürmektedir.

Test yönelimli programlama; hemen işlevi düzeltmek yerine, öncelikle bu hatalı durumu yakalayan bir birim testinin eklenmesini şart koşar. Çü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): Assertion 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

... çözüm