Geliştirme yaparken kodun çalışacağı tüm koşulları her zaman göz önünde bulunduramayabiliyoruz. Bu yüzden yazılım testleri var ve çok geç olmadan bu hataları fark etmek için önceden yazılım testleri hazırlayıp bu testlerin kapsama alanını uygulamanın genişlemesiyle paralel olarak artırmamız gerekiyor.

Bu yazıda en temel test pratiği olan birim testinden bahsedeceğim. Yazının ileriki bölümlerinde farklı test teknikleri ve üçüncü parti test araçlarına göz atacağız.

Test Nedir?

Testlerin neden önemli olduğundan bahsetmeden önce testin ne olduğunu açıklamak gerekiyor. Ana uygulamadan bağımsız, uygulamanın belirli kısımlarının farklı durumlarda hata üretip üretmediğini ya da alınan sonuçların normal veya doğru olup olmadığını kontrol etme pratiğine testing diyoruz.

Yazı boyunca test derken, otomatik testleri kast ediyor olacağım. Otomatik testler, bilgisayarın yine bizim yazdığımız kodlarla yaptığı testlerdir. Bir de manuel testler vardır. Bunlar da, ilgili bir kişinin (insan) son kullanıcı rolü yaparak, uygulama ile etkileşime geçip, kasti olarak sıra dışı durumlar oluşturarak olası hataları bulmaya çalışması işlemidir. Tabii ki, sıra dışı derken normal bir kullanıcının karşılaşabileceği uç senaryolardan bahsediyorum.

Testler ile ne tür hatalar yakalanabilir?

Otomatik testlerle, örneğin bir if bloğunu başlatırken satır sonuna iki nokta üst üste : koymayı unutmak ya da kodun herhangi bir yerinde girintileme hatası yapmak gibi söz dizimi hataları(syntax errors) yakalanabilir.

Herhangi bir problemin çözümü için oluşturduğunuz bir fonksiyonun yanlış değer döndürmesi gibi algoritmik ya da mantıksal hatalar(logical errors) yakalanabilir. Örnek olarak, rastgele sayı üreten bir fonksiyonu ya da fibonacci sayıları üreten bir fonksiyonu verebiliriz. Bu gibi fonksiyonların doğru çalışıp çalışmadığını insan gözüyle fark etmek çoğu zaman zordur.

Bunun yanında, dikkatsizlikten kaynaklanan bazı hataları da yakalamak mümkündür.

Örneğin aşağıda bir yazılımcı, sisteme giriş yapılma esnasında kullanıcının telefonuna bir sms göndermiş ve hangi numaraya gittiğini belli etmek için telefon numarasının son hanesini yazdırmak istemiş, ancak bir an için string uzunluğunu sıfır tabanlı düşünmesi gerektiğini unutuvermiş. Dolayısıyla bu kod bir IndexError fırlatacak:


print('Onay kodu gönderildi: *** *** ** *{}'.format(
    phone_number[len(phone_number)]
))
Test etmeden canlıya aldıktan sonra.

Testleri bir angarya olarak gören bu yazılımcı bunun bedelini kullanıcıların bir süre sisteme giriş yapamamasıyla ödedi. Öte yandan, bildiğiniz üzere, yukarıdaki hatayı almamak için phone_number[len(phone_number) -1 ] ya da phone_number[-1:] kullanabilirdi.

Birim Testi Nedir?

Yukarıdaki tüm durumlardaki hataların yakalanması kodun dikkatlice test edilmesiyle sağlanabilir. Yazılan kodun izole edilmiş bir kısmının test edilmesine birim testi(unit testing) denir. Buradaki birim(unit) bazen komple bir modül, bazen bir sınıf bazen de sadece bir fonksiyon olabilir.

Test edilecek birimi seçerken dikkate alınacak kıstas, o kod parçasının kendi içinde mantıksal bütünlük içeren bir işi yapabiliyor olması olmalıdır.

Örneğin, "başarılı kullanıcı girişini" test eden bir birim testi, kullanıcı kaydının veritabanında var olduğunu ve aktif durumda (örneğin kullanıcının sisteme girişi yasaklı olmamalı) olduğunu doğrulayıp ardından kullanıcı adı ve parolasının eşleştiğini doğrulayıp, son olarak ilgili verinin oturum (session) içinde başarıyla depolandığından emin olmalıdır.

Tek seferde test edilen kod parçacıklarının yaptıkları işlerin çok fazla olması, test sonuçlarından alınan verilerin kafa karıştırmasına sebep olabilir.

Birim testi nasıl yapılır?

Aşağıda, parametre olarak verilen eposta adresinin geçerli olup olmadığını doğrulayan bir fonksiyon var. Hadi bunu test edelim.


def validate_email(address):
    import re
    return bool(re.match(r'[^@]+@[^@]+\.[^@]+', address))

Doğrulayıcı fonksiyonlarımın hepsi validators.py içinde duruyor. Bu fonksiyonlarımı test etmek için de, birim testlerimi test_validators.py dosyamda tutuyorum.


import unittest

from validators import validate_email


class ValidatorsTestCase(unittest.TestCase):
    def test_validate_email(self):
        self.assertTrue(validate_email('birisi@gmail.com'))


if  __name__ == '__main__':
    unittest.main()

Birim testlerimi yapmak için python standart kütüphanesindeki unittest modülünü kullanıyorum. Burada dikkat etmemiz gereken; test sınıfının unittest.TestCase sınıfından miras alıyor olması ve test methodlarının isminin test ile başlıyor olması. Aksi halde bu fonksiyonlar test olarak görülmeyip, ilgili kısımlar teste tabi tutulmaz. Ancak hata da almazsınız.

Testi çalıştırdığınızda "Ran 1 test in 0.000s" benzeri bir çıktı aldığınızda kaç tane testin çalıştırıldığını ve ne kadar zaman aldığını görebilirsiniz.

Miras aldığımız TestCase sınıfının assertTrue methodu, verilen parametrenin True olup olmadığını kontrol eder.

Aşağıdaki tablodan ya da resmi dökümantasyon sayfasından tüm assert methodlarının ne işe yaradığına göz atabilirsiniz.

Kullanılabilecek doğrulama methodlarının listesi. (Python3)

Şimdi, yukarıda yazdığımız testi python test_validators.py komutu ile çalıştırıp sonucuna bakalım. Her şey normal gittiyse aşağıdakine benzer, testin başarılı geçtiğine dair bir çıktı almış olmalısınız:


$ python test_validators.py

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

E herhalde, bu testi babam da geçer. Şimdi, tüm kullanıcıların mükemmel olduğunu varsayımını bırakıp farklı uç durumlar için birkaç test daha ekleyelim:


import unittest

from validators import validate_email


class ValidatorsTestCase(unittest.TestCase):
    def test_validate_email(self):
        # Bu format kesinlikle doğru.
        self.assertTrue(validate_email('birisi@gmail.com'))

        # Alt tire, tire ya da rakam içerebilir.
        self.assertTrue(validate_email('b---i_risi32@gmail.com'))

        # Soru işareti ya da ünlem içeremez.
        self.assertFalse(validate_email('birisi?ulan!@gmail.com'))

        # Mutlaka @ işareti içermelidir.
        self.assertFalse(validate_email('birisigmail.com'))

        # @ işareti adresin başında olamaz.
        self.assertFalse(validate_email('@birisigmail.com'))

        # @ işaretinden bir adet nokta içermelidir.
        self.assertFalse(validate_email('birisii@gmailcom'))


if __name__ == '__main__':
    unittest.main()

Testin yeni hali nispeten daha kapsayıcı oldu, assertFalse kullanımına dikkat edin.

Testi çalıştırdığımda başarısız olduğunu görüyorum:


$ python test_validators.py

F
======================================================================
FAIL: test_validate_email (__main__.ValidatorsTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_validators.py", line 15, in test_validate_email
    self.assertFalse(validate_email('birisi?ulan!@gmail.com'))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Test çıktısı bana testin başarısız olduğunu ve bahsi geçen hatanın test_validators.py dosyasının 15. satırında, test_validate_email methodu içinde olduğunu söylüyor.

Bir test başarısız olduğunda AssertionError fırlatılır.

Testte birisi?ulan!@gmail.com adresi doğrulamadan geçmemesi gerekirken geçmiş. Yani kodum eposta adresi içindeki özel karakterleri geçerli sayıyor. Bu durumda doğrulama yaptığım validate_email fonksiyonunu bu durumları da kapsayacak şekilde geliştirmem gerekiyor.

· · ·

Bazı testler için önceden hazırlık yapmamız ya da test verilerini oluşturmamız gerekebilir. Testler yapılmadan önce setUp, yapıldıktan sonra da tearDown methodları çalıştırılır.

Aşağıda tüm geçerli eposta adreslerini doğrulamasını beklediğimiz bir test görüyorsunuz.


class ValidatorsTestCase(unittest.TestCase):
    def setUp(self):
        print('- Test verileri hazırlanıyor...')
        self.valid_emails = [
            'birisi@gmail.com',
            'merhaba_dunya@google.com',
            'Dj_Y4RALI_BELA@hotmail.com',
        ]

    def tearDown(self):
        print('- Test verileri siliniyor...')
        self.valid_emails = []

    def test_valid_emails(self):
        for address in self.valid_emails:
            self.assertTrue(validate_email(address))

Gördüğünüz üzere, önce test edilmek istenen eposta adresleri belirleniyor. Bir döngü yardımı ile hepsinin tek tek geçerli olduğundan emin olunduktan sonra oluşturulan eposta adresleri siliniyor.

Sanırım şimdilik bu kadar yeter. Kolay gelsin.

Siz daha önce birim testi yaptınız mı? Yaptıysanız en çok nelere dikkat ettiniz?

Cevaplarınızı yorumlarda bekliyorum. :)