Test Edilebilir Kod Yazma
Yayınlanan: 2022-11-03Birim testi, herhangi bir yazılım geliştiricisinin araç kutusundaki önemli bir araçtır. En iyi yazılım tasarım uygulamalarını, kalıplarını ve ilkelerini izleyen bir kod tabanıyla uğraşırken birim testleri yazmak nispeten kolaydır. Asıl sorun, kötü tasarlanmış, test edilemez kodu birim test etmeye çalışırken ortaya çıkar.
Bu blog, daha fazla birim test edilebilir kodun nasıl yazılacağını ve test edilebilirliği iyileştirmek için hangi kalıplardan ve kötü uygulamalardan kaçınılacağını tartışacaktır.
Test edilebilir ve test edilemez kod
Uzun vadede bakımının yapılması gereken büyük ölçekli uygulamalar üzerinde çalışırken, sistemin genel kalitesini yüksek tutmak için otomatik testlere güvenmeliyiz. Birden fazla birimi bir bütün olarak test ettiğiniz entegrasyon testleriyle karşılaştırıldığında, birim testleri hızlı ve kararlı olma avantajına sahiptir. Hızlı çünkü ideal olarak sadece test edilen sınıfı somutlaştırıyoruz ve genellikle harici bağımlılıkları, örneğin veritabanı veya ağ bağlantısı ile alay ettiğimiz için kararlı.
Birim ve entegrasyon testleri arasındaki tam farkı bilmiyorsanız, bu konu hakkında Teste Giriş blogumuzda daha fazla bilgi edinebilirsiniz.
Test edilebilir kod, kod tabanımızın geri kalanından izole edilebilir. Başka bir deyişle, en küçük birimler bağımsız olarak test edilebilir. Test edilemeyen kod, bunun için iyi bir birim testi yazmak zor, hatta imkansız olacak şekilde yazılmıştır.
Test edilebilir kod yazarken kaçınmamız gereken bazı anti-kalıpları ve kötü uygulamaları gözden geçirelim.
Örnekler Java'da yazılmıştır, ancak burada bahsedilen kodlama kuralları, herhangi bir nesne yönelimli programlama dili ve test çerçevesi için geçerlidir. Bu blog yazısında örnekler için assertj ve JUnit5 kullanacağız.
Bağımlılık enjeksiyonu
Bağımlılık ekleme, test izolasyonunu sağlamak için en önemli tasarım modellerinden biridir. Bağımlılık enjeksiyonu, bir nesnenin diğer nesneleri (bağımlılıkları) kendisi oluşturmak yerine yapıcı parametreleri veya ayarlayıcılar aracılığıyla aldığı bir tasarım modelidir.
Bağımlılık enjeksiyonu ile, bir nesnenin bağımlılıklarını taklit ederek test edilen sınıfı kolayca izole edebiliriz.
Bağımlılık enjeksiyonu olmayan bir örneğe bakalım:

Car sınıfının yapıcısında motor bağımlılığı oluşturulduğundan, Araba ve Motor sınıflarının sıkı bir şekilde eşleştiğini söyleyebilirsiniz. Birbirlerine son derece bağımlıdırlar - birini değiştirmek diğerinde bir değişiklik gerektirir.
Test açısından bakıldığında, yukarıdaki örnek, somut Motor uygulamasını bir test çiftiyle değiştiremeyeceğinden, Araba sınıfını tek başına test edemezsiniz.
Ancak, bağımlılık enjeksiyonu ve polimorfizm kullanımı ile izolasyonu sağlayabiliriz:

Artık birden fazla motor uygulaması ve dolayısıyla farklı motorlara sahip arabalar oluşturabiliriz:

Engine soyutlamanın alaycı bir uygulamasını oluşturup bunu Car sınıfımıza aktarabileceğimiz için, izolasyonda test etmek artık mümkün:

Başka nesneler (bağımlılıklar) gerektiren nesnelerle uğraşırken, bunları ideal olarak bazı soyutlamaların arkasına gizlenmiş yapıcı parametreleri (bağımlılık enjeksiyonu) aracılığıyla sağlamalısınız.
Bu kalıbı takip ederek kodunuz daha okunabilir ve zamanla değişime uyarlanabilir hale gelir. Ayrıca, yapıcılarda fiili çalışma yapmaktan kaçınmalısınız - alan atamalarından daha fazlası fiili çalışmadır. Yapıcılardaki 'new' anahtar sözcüğü, her zaman test edilemeyen kodun bir uyarı işaretidir.
Burada dikkat edilmesi gereken bir nokta, bazı durumlarda sıkı bağlantının (örneğin, yapıcılardaki yeni anahtar kelimeler, mantık yalıtımı için iç sınıflar, nesne eşleyiciler) kötü bir uygulama olmadığıdır – “bağımsız” bir sınıf olarak anlamlı olmayacak sınıflar.
küresel durum
Genel bir durumu paylaşmak, özellikle çok iş parçacıklı ortamlarda, genellikle kesintili testler (bazen başarılı, bazen başarısız) üretebilir.
Test edilen birden fazla nesnenin aynı global durumu paylaştığı bir senaryo hayal edin - nesnelerden birindeki bir yöntem, paylaşılan global durumun değerini değiştiren bir yan etkiyi tetiklerse, diğer nesnedeki bir yöntemin çıktısı tahmin edilemez hale gelir. Küresel durumu bir şekilde değiştirdikleri veya bazı küresel durumlara vekil oldukları için saf olmayan statik yöntemler kullanmaktan kaçının.
Bu saf olmayan statik yönteme bakalım:

Esasen, bu yöntem mevcut sistem tarihini ve saatini okur ve bu değere dayalı bir sonuç döndürür. LocalDateTime.now() statik çağrısı, testlerimizin yürütülmesi sırasında farklı sonuçlar üreteceğinden, bu yöntem için durum tabanlı uygun bir birim testi yazmak çok zor olacaktır. Bu yöntem için test yazmak, sistem tarih ve saatini değiştirmeden mümkün değildir.
Bunu düzeltmek için, argüman olarak tarih saatini timeOfDay yöntemine ileteceğiz:


timeOfDay statik yöntemi artık saftır – aynı girdiler her zaman aynı sonuçları verir. Artık, testlerimizde bağımsız değişken olarak izole edilmiş dateTime nesnelerini kolayca geçirebiliriz:

Demeter Yasası
Demeter Yasası veya en az bilgi ilkesi, nesnelerin yalnızca ilk nesneyle yakından ilişkili nesneler hakkında bilmesi gerektiğini belirtir. Başka bir deyişle, bir nesnenin yalnızca ihtiyaç duyduğu nesnelere erişimi olmalıdır. Örneğin, bir bağlam nesnesini argüman olarak kabul eden bir yöntemimiz var:

Bu yöntem Demeter Yasasını ihlal eder çünkü işini yapmak için gerekli bilgiyi elde etmek için bir nesne grafiğini yürümesi gerekir. Gereksiz bilgileri sınıflara ve yöntemlere aktarmak, test edilebilirliği zedeler.
Diğer nesnelere referanslar içeren devasa bir BillingContext nesnesi hayal edin:

Gördüğümüz gibi, testimiz gereksiz bilgilerle dolu. Karmaşık nesne grafikleri oluşturan testlerin okunması zordur ve gereksiz karmaşıklığa neden olur.
Bir önceki örneğimizi düzeltelim:

Her zaman doğrudan bağımlılıkları sınıflarınıza ve yöntemlerinize aktarmalısınız. Bununla birlikte, birçok argümanı yöntemlere geçirmek de iyi bir uygulama değildir - ideal olarak, maksimumda iki argüman iletmeli veya yakın ilişkili argümanları veri nesnelerine sarmalısınız.
Tanrı itiraz ediyor
Tanrı nesnesi, başka birçok farklı nesneye gönderme yapan, birden fazla sorumluluğu olan ve değişmesi için birden çok nedeni olan bir nesnedir. Sınıfın ne yaptığını özetlemek zorsa veya özetlemek “ve” kelimesini içeriyorsa, sınıfın muhtemelen birden fazla sorumluluğu vardır.
Tanrı nesnelerini test etmek zordur, çünkü çok sayıda ilişkisiz bağımlılıkla uğraşıyoruz, çeşitli soyutlamalar ve endişeleri karıştırıyoruz ve birçok yan etki üretiyorlar. Sonuç olarak, test durumlarımız için istenen durumu elde etmek zordur.
Örneğin:

UserService'in birden fazla sorumluluğu vardır - yeni kullanıcıları kaydetme ve e-posta gönderme. Kullanıcı kaydını test ederken, e-posta hizmetiyle ilgilenmemiz gerekiyor ve bunun tersi de geçerli:

İkiden fazla ilgisiz bağımlılığa sahip bir UserService düşünün. Bu bağımlılıkların kendi bağımlılıkları vardır, vb. Okunamayan, alakasız bilgilerle dolu ve anlaşılması çok zor bir testle sonuçlanacaktık. Bu nedenle, her sınıfın tek bir sorumluluğu ve değişme nedeni olmalıdır. Değişmesi için yalnızca bir nedeni olan bir sınıf, Tek sorumluluk ilkesi adı verilen beş yazılım tasarım ilkesinden biridir.
SOLID ilkeleri hakkında daha fazla bilgiyi buradan edinebilirsiniz.
Çözüm
Yazılım tasarımı en iyi uygulamalarını izleyen bir kod tabanı, birim testleri yazmayı çok daha kolay yönetilebilir hale getirir.
Öte yandan, bahsedilen anti-kalıpları kullanarak bir kod tabanı için çalışan birim testi yazmak çok zor, hatta bazen imkansız olabilir. İyi test edilebilir kod yazmak çok fazla pratik, disiplin ve ekstra çaba gerektirir. Test edilebilir kodun en önemli avantajı, test edilebilirlik kolaylığı ve bu kodu anlama, koruma ve genişletme yeteneğidir.
Bu blogun test edilebilir kod yazmanıza yardımcı olacağını umuyoruz.