TDD: Test Edilebilir Kod Üretmek

Daha önceki 2 yazımda TDD hakkında konuştuk ve PHPUnit aracını nasıl kullanacağımıza dair basit bir giriş yapmıştık. Aşağıdaki linklerden ulaşabilirsiniz.

Test Driven Development (TDD) Nedir?

PHP ve TDD: PHPUnit Nasıl Kullanılır?

Giriş

Test yapabilmenin en güzel özelliği, test edilebilir kod oluşturduğunuzda kodunuzun kendiliğinden onarılması ve genişletilmesi kolay bir koda dönüşmesi. Kısacası test yapabilmeniz için kalite bir kod üretmeniz gerekiyor. Kaliteli kod üretmek bir kaç prensip bunları uyguladığınızda kodunuz test edilebilir olacaktır.

Test edilebilir kod üretmek için SOLID dediğimiz prensipleri uyguluyoruz. SOLID’in açılımı;

  • Single Responsibility
  • Open/Closed Principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Şimdi bu konulara tek tek değineceğiz.

Single Responsibility

Türkçesi tek sorumluluk anlamına geliyor. Yani yazdığınız sınıfların/classların sadece tek bir işten sorumlu olması gerekiyor, ve bu sorumluluğu tamamen kapsaması gerekmektedir. Wikipedia‘dan alıntı yaparak bir örnek paylaşmak istiyorum:

Bir yazılım parçası olarak rapor hazırlayıcı düşünelim. Bu hazırlayıcı raporları derliyor ve daha sonra çıktı alıyor. Bu modül 2 sebepten dolayı değişebilir; Ya raporun içeriği değişebilir, ya da raporun formatı değişebilir. Bu 2 değişiklik çok farklı şeylere hitap etmektedir. Bir tanesi dış görünüş ile alakalı, diğeri ise işin özüyle alakalıdır. Bu yüzden bu iki iş çok farklı 2 sorumluluktur. Bu yüzden bu 2 işlev farklı modüllere veya classlara ayrılması gerekmektedir.

Open/Closed Principle

Tasarım desenlerinin temelini oluşturan prensiptir. Türkçesi tam olarak; “Değişime kapalı, genişletmeye açık” anlamına gelmektedir. Yazılımdan ek görevler istendiğinde yazılım değiştirilmeden sadece üstüne birşeyler eklenerek o görevlerin eklenmesi demektir.

Liskov substitution principle

Kısaca yerine konabilirlik prensibi diyebiliriz. Hatta İngilizce’de de oyuncu değişikliği için substitution kullanılır. Burada da yaptığımız tam olarak budur. Kısaca; “oyuncu değişikliği” :). Bir class’ımız var ve bu class, B classını kullanmaktadır. Ve bu B class’ı A classından türetilmiştir. Aynı zamanda da C classı da A classından türetilmiştir. Sonuç olarak bizim classımıza B yerine C‘yi gönderidiğimizde de hiç bir sorun çıkarmadan çalışmaya devam etmesi gerekmektedir. Kısaca, yarattığımız modüllerde aynı türden türemiş sınıflar kullanıldığında sınıfımızın bunlardan etkilenmemesi gerekmektedir.

Interface segregation principle

Hiç bir somut yani hayata geçirilmiş bir sınıfta ihtiyacı olmayan methodları hayata geçirmesini zorlanmamasıdır. Mesela Insan ve Kopek sınıfları Canli sınıfından türediklerini varsayalım. Ancak Canli sınıfı alt sınıflarını konus() methodunu hayata geçirmesi için zorlamamalıdır çünkü köpekler konuşamaz.

Dependency inversion principle

Yazılım mimarisinde, üst seviye sınıflar, alt seviye sınıfları kullanabilecek şekilde tasarlanırlar. Yani üst seviye sınıflar işlerini yapabilmeleri için alt seviye sınıflara ihtiyaç duyarlar. Yani bu şekilde bir yandan tekrar kullanılabilir alt sınıflar inşa ederek, bir yandan da onarılması kolay yazılımlar hayata geçiriyoruz.

Dependency Injection

Bir üst başlıklarda anlattıklarımızın pratikte uygulanmasıdır diyebiliriz. Şimdi bir PHP kodu ile buradaki prensipleri nasıl uygulayacağımıza bir göz atalım;

Basit bir post yazma uygulaması yaptığımızı varsayalım. Öncelikle bir Post için interface yaratıyorum:

/**
 * Interface segregation principle:
 * Temel olarak post'un ihtiyaç duyduğu şeyler dışında
 * hiç birşey yapmaya zorlamıyor
 */
interface PostInterface {

    public function yeni($baslik, $icerik, $user_id);

    public function duzenle($id, $baslik, $icerik);

    public function sil($id);
}

Daha sonra MySQL kullanarak çalışan bir Post_MySQL classı tanımlıyorum.

class Post_MySQL implements PostInterface {

    public function duzenle($id, $baslik, $icerik) {
        // UPDATE post SET baslik = ...
    }

    public function sil($id) {
        // DELETE FROM post ...
    }

    public function yeni($baslik, $icerik, $user_id) {
        // INSERT INTO post ...
    }

}

Burada bir alt seviye sınıf oluşturduk. Birazdan bir üst seviye sınıf oluşturarak bu alt seviye sınıfı sömürmesini sağlıyacağız. (Dependency inversion principle)

/**
 * Üst seviye sınıfımız:
 * Alt seviye bir sınıf olan PostInterface'i kullanıyor
 */
class PostController {

    public $post;

    public function __construct(PostInterface $post) {
        $this->post = $post;
    }

    public function yeni($baslik, $icerik, $user_id) {
        $this->post->yeni($baslik, $icerik, $_SESSION['user_id']);
    }

    public function sil($id) {
        $this->post->sil($id);
    }

    public function duzenle($post_id, $baslik, $icerik) {
        $this->post->duzenle($_GET['id'], $baslik, $icerik);
    }

}

Şimdi ise bunları kullanalım:

// Dependency Injection
$controller = new PostController(new Post_MySQL());
$controller->...

Bugün patron aradı. MySQL yerine MongoDB kullanacağız bundan sonra dedi.

/**
* Liskov substitution principle:
* tek yaptığımız oyuncu değişikliği
* Post_MongoDB sınıfı'da PostInterface'i 
* hayata geçirdiği için hiç birşeyden etkilenmeden hayatımıza devam ediyoruz :)
*/
$controller = new PostController(new Post_MongoDB());
$controller->...

Sonuç

Biraz teori, biraz pratik ile kaliteli ve test edilebilir bir kod nasıl üretilir öğrendik (umarım). Bir sonraki yazımda ise bu yaptıklarımızın test yaparken ne işe yaradıklarını anlatacağım. Hepinize iyi çalışmalar.

6 thoughts on “TDD: Test Edilebilir Kod Üretmek

  1. Buse

    PostController sınıfı için ,kod içindeki açıklamanıza göre
    Alt seviye bir sınıf olan PostInterface‘i kullanıyor demişsiniz.

    Kullanımda ise
    $controller = new PostController(new Post_MySQL());
    kodlamışsınız. Peki o zaman PostInterface PostController sınıfı içinde ne işe yaraycak? çelişkili değil mi?

    Reply
    1. Can Geliş Post author

      PostController‘ın constructor’ı PostInterface türünden herhangi bir objeyi kabul ediyor. Post_MySQL sınıfı da PostInterface türünden olduğu için Post_MySQL‘i kabul ediyor. Şuradan görülebilir:

      class Post_MySQL implements PostInterface {
      

      Neden Interface tanımlıyoruz ?

      Interface tanımlamamızın sebebi kurallar belirlemek. Örneğin bir aracı, farklı teknolojiler kullanarak, aynı işlevleri yerine getirebilecek sınıflar yaratılabilmesi için kullanıyoruz. Örneğimizde programcı Post işlevlerini yerine getirmek için MySQL kullanıyor. Bunu yaparkende PostInterface‘in kurallarına bağlı kalıyor. Ancak PostController‘ın MySQL’den haberi yok çünkü onun görevi PostInterface‘te tanımlanmış methodları çağırabilmek. Daha sonra kullanılacak teknoloji değişiyor ve MongoDB oluyor. Daha sonra başka bir programcı aynı interface‘i kullanarak Post işlevlerini yerine getirmek için PostInterface‘in kurallarına bağımlı kalıyor ve Post_MongoDBsınıfını oluşturuyor. Ancak bu PostController‘ın umurunda değil çünkü onun tek bildiği şey PostInterface ve bu şekilde PostController sınıfı Post_MongoDB sınıfını da kabul edebiliyor. Bizde bu şekilde PostController sınıfında hiç bir değişiklik yapmadan teknolojiyi değiştirebilmiş olduk (Liskov substitution principle). Post_MongoDB sınıfı da şu şekilde yaratılıyor:

      class Post_MongoDB implements PostInterface {
      

      Biz bu olaya kısaca Polymorphism diyoruz.

      Reply
    2. Buse

      Cevabınız için teşekkür ederim. Ben class PostController içindeki bu kısmı kast etmiştim.

      public function __construct(PostInterface $post) {
      $this->post = $post;
      }

      Reply
      1. Can Geliş Post author

        Orada PostInterface yazması sadece PostInterface kabul ettiği anlamına gelmez. PostInterface yazması demek o türden herhangi bir objeyi kabul etmesi demektir. Bu OOP’un temel konularından bir tanesidir. Ayrıca PostInterface bir interface olduğu için zaten instantiable değildir. Bir örnek:

        interface Sayi {
            public function numara();
        }
        
        class Bir implements Sayi {
            public function numara() {
                return "1";
            }
        }
        
        class Iki implements Sayi {
            public function numara() {
                return "2";
            }
        }
        
        class Uc implements Sayi {
            public function numara() {
                return "3";
            }
        }
        
        class Yazdirici {
            public function __construct(Sayi $sayi) {
                echo $sayi->numara();
            }
        }
        
        new Yazdirici(new Bir());
        new Yazdirici(new Iki());
        new Yazdirici(new Uc());
        
        Reply
  2. MURATSPLAT

    Selamlar,

    Arasıra açıp bakıyorum. Fena halde NYP tekniklerine merak saldım..

    Şuan için kafama hiç oturmayan şey “abstract” kullanımı. İnsan bu ne için kullanır ki. Herhalde program yazdıkça bunu nerde kullanacağımı anlayacam..

    Reply
    1. Can Geliş Post author

      abstract methodlar, teorideki adıyla pure virtual functionlar, bir sınıfı abstract yapar yani o sınıftan obje yaratılamaz. Bu yüzden php'de bir sınıfta abstract method varsa sınıfta abstract olarak işaretlenmelidir. Kısaca abstract olan sınıfı new ile yaratamazsın.

      Bu şekilde obje yaratmak (instantiate) için değil, genişletmek (extend etmek) için sınıflar yaratılabilir ve senin yazdığın sınıfı genişletecek kişiye nereleri overwrite etmesi gerektiği konusunda yol gösteriyorsun.

      Not: Her abstract method overwrite edilmek zorundadır.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax