Kolay Unit Testing için Dependency Injection Kullanmak

Hatırlatma: YouTube Kanalıma abone olarak, her hafta eklenen yazılımcılar için programlama ve kariyer video eğitimlerime ulaşabilirsiniz.

Dependency Injection nedir ve ne gibi faydaları vardır sorularına genel bir cevap olarak yazdığım “Dependency Injection Nedir?” yazımı okuduysanız, o yazım içerisinde kısaca değindiğim Unit Test’e dair faydalarını bu yazımda biraz daha detaylandırmaya çalışacağım.

Kod yazarken çoğu zaman business logic ve database access logic gibi kısımlara sahip olacaksınız. Bunları isterseniz kendi ayrı modülleri içinde isterseniz karman çorman herşeyin aynı yerde olduğu şekilde yazın, siz adını koymasanız bile o kodlar orada olacaktır. Unit testing ile veri işlemede kullandığınız business logic olarak tanımlanan kodları test edersiniz. Bu yazının anlaşılması için gerekli olan alt yapıda Unit Testing ve Dependency Injection gibi iki konseptin anlaşılmış olması lazım.

Şimdi adım adım aşağıda gösterilen kodu düzenlemeye ve Unit Test için hazır hale getirmeye çalışalım:

public class AccountCreator{
private AccountChecker _accountChecker;
private DatabaseAccountRepository _accountRepository;

public AccountCreator(){
_accountChecker = new AccountChecker();
_accountRepository = new DatabaseAccountRepsitory();
}

public void CreateAccount(AccountInfo accountInfo){
if(HasAccountNumber(accountInfo)){
throw new InvalidAccountInfo("New accounts cannot have account numbers");
}
if(_accountChecker.Exists(SanitizeUserName(accountInfo.UserName)){
throw new UsernameExistsException("This username is already taken. Please use a different username");
}
_accountRepository.Create(GetAccountDataTransferObject(accountInfo));
}

private string SanitizeUserName(string username){
var sanitizedUsername = username.Trim().ToLower().HtmlEncode();
}

private bool HasAccountNumber(AccountInfo accountInfo){
return accountInfo.AccountNumber != null;
}

private AccountDTO GetAccountDataTransferObject(AccountInfo accountInfo){
return new AccountDTO {
FirstName = accountInfo.FirstName,
LastName = accountInfo.LastName,
UserName = accountInfo.UserName,
AccountCreated = accountInfo.AccountCreated
}
}
}

Yukarda ki kodun anlaşıldığını varsayıyorum. Bu kodun test edilebilir kısımlarına baktığımızda aşağıda ki bazı testler yazılabilir:

  1. Happy Path: Bu bir methodun belirli bir algoritmayı takip edip etmediğini test etmek demektir. Mesela yukarıda ki CreateAccount methoduna baktığınızda SanitizeUsername methodu çağrılıyormu, _accountReposiyory.Exists() gibi methodlar kullanılıyor mu bunları test edebilirsiniz. Yan etkisi olan methodların Happy Path için test edilmesi mantıklı olacaktır.
  2. Exceptionları fırlatması. CreateAccount methodu beklenmedik AccountNumber verildiğinde yada Username daha önceden sistem içerisinde tanımlı olduğunda hatalar döndermekte. Bunlarında kesinlikle test edilmesi lazım. Yani methoda bu hataları fırlatmasına sebep olacak verilerin gönderilmesi ve bu hataları fırlatıyormu diye bakılması lazım.
  3. Kişiye bağlı olarak private methodlarda test edilebilir. Normalde çok gerek yok. Nedeni ise önemli olan public methodların test edilmesi çünkü dış dünya ile alakadar olacak onlarlar onlar ki zaten bu public methodlar içlerinde private methodları kullanacağı için otomatik olarak test edilmiş olacaklar. Ama eğer utility methodları kullanıyorsanız diğer sınıflardan gelen, onların kesinlikle test edilmiş olması lazım.

Yukarıda ki kod karmaşık olmasın diye herşey dikkate alınarak yazılmadı. Örnek olması açısından anlaşılır ve basit tutulmaya çalışıldı. Şimdiye kadar anlattıklarım güzel ama kod incelendiğinde unit test için hazır hale getirilmesinden ciddi bazı sorunlar gözükecek. Bunlardan en göze batanı _accountChecker.Exists() ve _accountRepository.Create() methodlarının veri tabanı yada hangi veri kaynağı kullanıyorsa oraya I/O çağrısı yapacak olmaları. Unit Testlerin deterministik (Yani önceden bilinebilir sonuçlar döndermeleri) olmaları ve hızlı çalışmaları açısından bu şekilde I/O çağrıları yapmaları doğru değildir. O şekilde testlere Integration Test denir. Örneğin bir integration test içerisinden bir veriyi veri tabanına kaydeder, sonra onu geri çağırır ve üzerinden bazı algoritmalar ile işlem yaptıktan sonra testlerinizin sonuçlarını incelersiniz. Yada bunları bir senaryo şeklinde yaparsanız onlarada Scenario Testleri denir. Bu gibi daha kapsamlı testlerde her ne kadar aynı unit test framework’ları kullanılsada ikisinin yeri ve amaçlarında farklılıklar gözükür. Unit Test fonksiyonel bir parçanın yada diğer bir tabirle ünitenin test edilmesi demektir. Yazılan testlerin umulmadık hatalar vermemesi için çağrılan methodların kontrol edilebilmesi gerekmektedir. Mesela, siz yukarıda ki methodun InvalidAccountInfo exception’u fırlatmasını istiyorsunuz. Yukarıda ki koda bakınca testinizde kullandığınız UserName’in veritabanında olmasını sağlamak zorundasınız. Bunun içinde öncelikle o veriyi bir şekilde veri tabanına girmesini sağlamalı, sonra başka testleri bozmaması için kaldırmalı vs. derken ekstra birden fazla iş yapmış olacaksınız. Aynı şekilde testlerinizin diğer testleri etkilememesi de arzulunan sonuçlardan bir tanesidir. Diğer testleri etkileyen testler bakımı zorlaştırır ve çalışma zamanında paralel çalıştırma yaparak unit testlerin daha fazla CPU yada Core kullanılarak hızlıca bitirilmelerine engel olur.

Peki şimdiye kadar anlattığım sorunlara çözüm nedir? _accountRepository ve _accountChecker değişkenlerine sonradan test amaçlı oluşturduğum ve benim istediğim değerleri döndürecek ve IO çağrıları yapmayarak hızlı testin hızlı bir şekilde tamamlanmasını sağlayacak sınıflar atamak isteriz. Bunlara Mock objeler denir. Yada aynı şekilde test ikizleride diyenler vardır. Yani gerçek sınıfları kullanmak yerine test için hazırlanmış ve method implementasyonları test için farklı şekilde tanımlanmış ikiz sınıflar. Görünüş olarak aynı olmalarına rağmen içleri farklı olacaktır bu test sınıflarının gerçek sınıflar ile. O zaman aşağıda ki kodu yorumları ile beraber incelersek sorunun rahatlıkla Dependency Injection ile çözülebileceğiniz göreceğiz:

public class AccountCreator{
// Interface'ler tanımlıyoruz. Dolayısıyla kendi sınıflarımızı rahatlıkla kullanabiliriz.
private IAccountChecker _accountChecker;
private IAccountRepository _accountRepository;


// Dependency'lerimizi constructor method vasıtasıyla enjekte ediyoruz.
public AccountCreator(IAccountChecker accountChecker, IAccountRepository accountRepository){
_accountChecker = accountChecker;
_accountRepository = new accountRepository;
}

public void CreateAccount(AccountInfo accountInfo){
if(HasAccountNumber(accountInfo)){
throw new InvalidAccountInfo("New accounts cannot have account numbers");
}
if(_accountChecker.Exists(SanitizeUserName(accountInfo.UserName)){
throw new UsernameExistsException("This username is already taken. Please use a different username");
}
_accountRepository.Create(GetAccountDataTransferObject(accountInfo));
}

private string SanitizeUserName(string username){
var sanitizedUsername = username.Trim().ToLower().HtmlEncode();
}

private bool HasAccountNumber(AccountInfo accountInfo){
return accountInfo.AccountNumber != null;
}

private AccountDTO GetAccountDataTransferObject(AccountInfo accountInfo){
return new AccountDTO {
FirstName = accountInfo.FirstName,
LastName = accountInfo.LastName,
UserName = accountInfo.UserName,
AccountCreated = accountInfo.AccountCreated
}
}
}

Şimdi rahatlıkla iki tane yeni sınıf yazabilir ve bunları aşağıda ki gibi test etme de kullanabilirim:

public class AccountCreatorTest{
private IAccountChecker _accountCheckerMock;
private IAccountRepository _accountRepositoryMock;

[TestInitialize]
public void Initialize(){
_accountCheckerMock = new AccountCheckerMock();
_accountRepositoryMock = new AccountRepositoryMock();
}

private class AccountCheckerMock: IAccountChecker{
public bool Exists(){ return true; }
}

private class AccountRepositoryMock: IAccountRepository{
public void Create(){}
}

[TestMethod]
[ExceptedException(typeof(InvalidUsername))]
public void TestUsernameIsNotEmpty(){
var accountInfo = new AccountInfo() {UserName="Test"};
var accountCreator = new AccountCreator(_accountChecker, _accountRepository);
accountCreator.CreateAccount(accountInfo);
}
}

Yukarıda ki kod da bayağı anlaşılır aslında. Dolayısıyla yukarıda okunarak anlaşılabilen kodu burada tekrardan açıklayarak zamanınızı almak istemem. Ama dikkat edilmesi gereken en önemli özellik bağımlı olduğum sınıfların mock versiyonları rahatlıkla oluşturup bunları daha determistik test methodları yazmada kullanabilmem oldu. Not olarak düşmek istediğim ufak bir ayrıntıda SanitizeUserName() methodunun ayrı bir utility sınıfı içerisinde static olarak konulması ve test methodları ile test edilmesinin faydalı olacağına dair kanaatim.

Bağımlı olduğunuz sınıflar arttıkça yazmak zorunda kalacağınız Mock sınıflarıda artacak. Dolayısıyla bunları daha kolay bir şekilde oluşturmak için yazılmış kütühaneler mevcut. Bunlardan kullandığım teknolojiler içerisinde en çok tercih edilenin Moq isminde bir kütüphane olduğunu gördüm ve kendim de uzun zaman boyunca kullandım bu kütüphaneyi. Yorumumu duymak isteyenler için diyorum, bence gayet kullanışlı ve istenilen herşeyi yapıyor. Hatta yukarıda bahsettiğim Happy Path testide rahatlıkla yapılabilir Moq ile çünkü bir methodun çağırılıp çağrılmadığı testinide rahatlıkla yapmanıza imkan tanıyor. Belki başka bir yazımda Moq kütüphanesini nasıl kullanabilirsiniz bunu açıklamaya çalışırım. Şimdilik esenlikle kalın. Sorunlarınız aşağıda ki yorum kısımlarına yazabilirsiniz.

Written by

Senior Manager in Software Engineering. Former Technical Lead. Author of the book: Hands-on with Go http://amzn.to/2QYFoaV YT: http://youtube.com/c/tarikguney

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store