Mockito – ilk Adımlar
Mockito adından da anlaşılacağı üzerine nesnelerinizi mock’lamanıza yarayan bir kütüphane. Peki nedir mock? Neden mock’lamaya ihtiyaç duyuyoruz? Ya da Mock’lama bize ne artı getiriyor? İşte bu yazıda ufak ufak bunları konuÅŸacağız. Ufaktan diÄŸer test terimlerine deÄŸineceÄŸiz.
Mock’lama Nedir?
Terimlere çok takılıp kalmak istemiyorum. Ama neyi neden yaptığımızı ve bize neler saÄŸladığını öğrenmemiz önemli. Mock tamamen beklentilerimiz ile ilgili. Unit testlerimizi yazarken ağırlıklı olarak kodun belirli bir bölümüne odaklanıyor. ÖrneÄŸin eÄŸer servis katmanımızı test ediyorsak, verinin nereden ya da nasıl geldiÄŸi bizim pek umurumuzda olmuyor. Aynı ÅŸekilde MVC tasarım ÅŸablonunda, Controller’ımızı test ediyorsak sınıfımızın servis üzerinden ne ÅŸekilde konuÅŸtuÄŸu da bizi pek ilgilendirmiyor. Tabi’ki bu kısımları da test etmemiz gerekiyor fakat onları ayrı unit testler de zaten test ediyoruz. İşte bu gibi durumlarda, ilgili katmanı mock’luyoruz. Mocklarken yaptığımız aslında basitçe ne mockladığımız methodun hangi tür çağırıldığında ne ÅŸekilde davranmasını beklediÄŸimizi belirtmektir. Bunu yapan bir çok kütüphane olmasına karşın benim bu tutorial üzerinden anlatacağım Mockito olacak.
Mock’lama genelde Stub oluÅŸturma ile karıştırılmaktadır. İkisi de aynı sonucu üretmek için kullanılabilir ama asıl ürettikleri çözüm tamamen birbirinden farklıdır. Stub daha çok sadece çalışılması istenilen alan için üretilen nesnelerdir. Yeri geldiÄŸinde bir servis sınıfı için stub oluÅŸturulabileceÄŸi gibi yeri geldiÄŸinde sadece modellerimizde Stub’lanabilir. Bu örnek boyunca stub’larda oluÅŸturacağız. Stublarımız üzerinden Mock’ladığımız nesnelerin davranışlarını belirleyeceÄŸiz.
Projenin Yaratılması
Projemiz basit bir java konsol uygulaması olacak. Aslında projemiz herhangibir ÅŸekilde olabilir. Ister web olsun iseter konsol uygulaması olsun bizi ilgilendiren kısmı testleri olacak. Gidip ağır bir servis katmanı üzerinden çalışmayacağız.Projemizi her zaman olduÄŸu gibi maven kullanarak geliÅŸtireceÄŸiz. Fakat geliÅŸtirme ortamımız olarak eclipse yerine Intellij Idea kullanacağız. Eclipse’i hala iÅŸ yerimde kullanmaya devam etsem de intellij’e alışmasının çok kolay olduÄŸunu farkettim. Özellikle Maven ve Spring desteÄŸi inanılmaz güzel. Aynı ÅŸekilde eclipse’e göre Content Assist’i çok daha baÅŸarılı. Eclipse’te JSF geliÅŸtirirken yer yer xhtml sayfalarında content assistin beni delirttiÄŸi oluyordu. Fakat daha intellij’de böyle bir problem yaÅŸamadım.
Neyse projenin yaratılmasına geri dönelim. Amacımız Mockito kullanılarak nasıl Mock’lama yapılacağı olduÄŸundan, servis katmanımızı yazmak yerine sadece Interface olarak belirleyeceÄŸiz. Sonrasında Controller sınıfımız bu servis sınıfını kullanarak yazacağız. Controller sınıfımızı bitirdikten sonra testlerimizi yazacağız ve iÅŸte asıl burada Mockito kullanmaya baÅŸlayacağız.
Maven
Projeden kısaca bahsettiÄŸimize göre Maven ile projeyi oluÅŸturmaya baÅŸlayabiliriz. Maven ile, maven-archetype-quickstart archetpye’ını kullanarak projemizi oluÅŸturuyoruz. Benim örneÄŸi yaparken kullandığım diÄŸer bilgiler aÅŸağıdaki gibidir. Eskiden projeleri oluÅŸtururken önce maven ile projeyi oluÅŸturup sonra eclipse’e eklerdim. Fakat ÅŸimdi bunu yapmama gerek yok. Intellij’in maven özelliÄŸi çok baÅŸarılı.
- Group Id: com.bahadirakin
- Artifact Id: test-with-mockito
Projemiz oluştuğuna göre, projemizin bağımlılılarını ekleyebilir ve nasıl build olacağını belirleyebiliriz. Bunun içintest-with-mockito klasörünün altındaki pom.xml dosyasını aşağıdaki şekilde değiştiriyoruz.
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.bahadirakin</groupId> <artifactId>test-with-mockito</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.java.version>1.6</project.java.version> <slf4j.version>1.7.5</slf4j.version> <mockito.version>1.10.8</mockito.version> <junit.version>4.11</junit.version> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>${project.java.version}</source> <target>${project.java.version}</target> </configuration> </plugin> </plugins> </build> <dependencies> <!-- Logging Dependencies --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-ext</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.0.13</version> </dependency> <!-- Test Dependencies --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> </dependencies> </project> |
Burada önemli olan kısım mockito bağımlılıkları. Gördüğünüz üzere mockito projeye eklenmesi açısından oldukça basit. Birazdan göreceÄŸiz kullanması da aynı ÅŸekilde çok basit. Projemizin servis katmanını mock’layacağımız için spring olsun diÄŸer orta katman mimarilerini olsun projemize dahil etmemize gerek yok.
Servis, Model ve Controller
Projemizi oluşturduğumuza göre, modelimizi, servis katmanımızı ve bu servis katmanımızı kullanan Controller sınıfımızı oluşturabiliriz. Burada şunu belirtmeliyim, anlaşılması kolay olsun diye böyle bir yapı seçtim. Normalde çok katmanlı bir uygulama geliştiriyorsanız buna benzer bir çok pattern ile zaten karşılaşmışsınızdır.
Mockito’ya ağırlık vermek için mantığımızı olabildiÄŸince basit tutmaya çalıştım. Buradaki metod tanımlamalarına ve yaklaşımlara lütfen takılmayın. Elimden geldiÄŸince ufak bir örnekle, mockitonun çoÄŸu özelliÄŸini kapsamaya çalışacağım. Bu sebepten herkesin alışık olduÄŸu bir örnek yapacağım. Basit bir login örneÄŸi yapacağım. Örnekte servis katmanımız Authentication kısmını saÄŸlarken, Controller katmanımız, authentication iÅŸleminin baÅŸarılı olup olmamasına göre sayfanın yönlendirmesinden sorumlu olacak. Bunun için ise bir User sınıfı tanımlayacağım ve iÅŸlemlerimiz bu user modeli üzerinden devam edecek.
Öncelikle projenin src/main/java klasörüne aşağıdaki paketleri ve sınıfları ekliyoruz. Sınıfların içeriğini daha sonradan vereceğim.
- com.bahadirakin.controllers: Projemizin tek controller sınıfı bu pakette yer alacak. LoginController sınıfını bu pakette oluşturuyoruz.
- com.bahadirakin.entitites: Projemizin entitylerini bu pakette yaratıyoruz. Basitlik katması açısından veritabanı ya da bir ORM katmanı yazmayacağız. Ama örnekleri daha iyi kavramak adına sizin yazmamanız için bir neden yok. Buraya bu örneğimizin tek modelini, User sınıfını koyuyoruz.
- com.bahadirakin.services: Projemizin servis katmanını belirler. EÄŸer ORM katmanımız olsaydı, bu paketteki servisler ORM katmanı ile konuÅŸacaktı. Aynı ÅŸekilde transaction yönetimi de yine bu katmanda yapılacaktır. Fakat biz bir tane servis tanımlayacağız. Hatta servisin implementasyonunu yazmayacağız bile sadece tanımlayıp bırakacağız. Bu sebepten buraya IUserService interface’ini oluÅŸturuyoruz.
- com.bahadirakin.exceptions: ÖrneÄŸimiz sırasında kullanacağımız exception’lar burada yer alacak. Normal bir projede farklı exception’lar için farklı paketleriniz olabilir. ÖrneÄŸin controller’lar ile ilgili exception’lar baÅŸka pakette, servislerle ilgili exception’lar baÅŸka pakette olabilir. ÖrneÄŸimiz boyunca yine tek bir exception kullanacağız. Bu sebepten buraya UserNotFoundException sınıfını oluÅŸturuyoruz.
Paketlerimizi ve içerisindeki sınıfları oluşturduğumuza göre şimdi sınıflarımızın içeriğini doldurmaya geçebiliriz.
Model
Projemizin tek modelinin içeriği aşağıdaki gibi olmalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package com.bahadirakin.entities; public class User { private Long id; private String username; private String password; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } |
Exception
Projemizin tek exception’ı UserNotFoundException‘ın içeriÄŸi aÅŸağıdaki gibi olmalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package com.bahadirakin.exceptions; public class UserNotFoundException extends Exception { public UserNotFoundException() { } public UserNotFoundException(String message) { super(message); } public UserNotFoundException(String message, Throwable cause) { super(message, cause); } public UserNotFoundException(Throwable cause) { super(cause); } } |
Service
UserService interface’inin içeriÄŸi aÅŸağıdaki gibi olmalıdır.
1 2 3 4 5 6 7 8 9 10 | package com.bahadirakin.services; import com.bahadirakin.entities.User; import com.bahadirakin.exceptions.UserNotFoundException; public interface IUserService { boolean authenticate(final User user) throws UserNotFoundException; } |
 Controller
Sıra esas test etmek istediğimiz sınıfı yazmaya geldi. LoginController sınıfımız aşağıdaki gibi olmalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package com.bahadirakin.controllers; import com.bahadirakin.entities.User; import com.bahadirakin.exceptions.UserNotFoundException; import com.bahadirakin.services.IUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; public class LoginController implements Serializable { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); private IUserService userService; public LoginController(IUserService userService) { this.userService = userService; } public String authenticate(final User user) { try { if (userService.authenticate(user)) { return "homePage"; } else { return "errorPage?message=wrongPassword"; } } catch (UserNotFoundException e) { logger.error("User not found for usernmae: {}", user.getUsername(), e); return "errorPage?message=userNotFound"; } } } |
Şimdi LoginController sınıfını biraz inceleyelim.
- LoginController doğrudan IUserService sınıfı ile ilişkili. Hatta IUserService implementasyonu sağlamadan, LoginController sınıfını oluşturamıyoruz bile. Eğer spring ile yönetilen bir ortamımız olsaydı burada ConstructorInjection yapmamız gerekebilirdi.
- LoginController içerisindeki authenticate methodu, User nesnesi içerisinde authentication işlemi yapıyor. Eğer user bulunduysa anasayfaya (homePage), bulanamadıysa hata sayfasına (errorPage) yönleniyor. Bunun yanı sıra farklı hatalar için farklı mesajlar üretiliyor.
Mockito Ile Testlerin Yazılması
Tutorial’ımızın başında testlerimizi Mockito ile yazacağımızı belirtmiÅŸtik. Öncelikle test paketlerimizi ve sınıflarımızı oluÅŸturalım. Bunun için projeye öncelikle src/test/java klasörünü ekliyoruz. Daha sonra com.bahadirakin.controllers paketini bu klasöre ekliyoruz. Burada olabildiÄŸince bir pattern üzerinden gitmeye çalışıyorum. Bunun Mockito ile bir alakası yok. Sadece test converage kısmında bana kolaylık saÄŸlıyor. Paketimizi oluÅŸturduktan sonra içerisinde LoginController sınıfını test edecek sınıfı yani LoginControllerTest sınıfını oluÅŸturuyoruz.
LoginControllerTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package com.bahadirakin.controllers; import com.bahadirakin.entities.User; import com.bahadirakin.exceptions.UserNotFoundException; import com.bahadirakin.services.IUserService; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.*; import org.mockito.internal.verification.Times; public class LoginControllerTest { @Mock private IUserService userService; @InjectMocks private LoginController loginController; @Before public void setup(){ MockitoAnnotations.initMocks(this); } @Test public void testAuhtentication() throws Exception { final User userStub = new User(); userStub.setUsername("bhdrkn"); userStub.setPassword("passowrd"); Mockito.when(userService.authenticate(userStub)).thenReturn(true); final String redirect = loginController.authenticate(userStub); Assert.assertEquals("homePage", redirect); Mockito.verify(userService, new Times(1)).authenticate(Mockito.any(User.class)); } } |
Burada dikkat edilmesi gereken kısımlar aşağıdaki gibidir.
- @Mock annotasyonunu mock’lamak istediÄŸimiz, sınıfta kullanıyoruz. Böylelikle Mockito bizim için bu sınıftan türemiÅŸ baÅŸka bir sınıf oluÅŸturuyor. Testimizi çalıştırmadan Mock’lanan sınıfından neler beklediÄŸimizi belirteceÄŸiz.
- @InjectMock annotasyonu mocklanan nesnenin hangi sınıfı oluÅŸtururken kullanacağını belirtmek için kullanıyoruz. Bu annotasyonu kullanmak zorunda deÄŸilsiniz. Testinizin setup phase’inde kendiniz oluÅŸturup Mock nesnelerinizi manuel olarakta set edebilirisiniz.
- @Before annotasyonu bildiÄŸiniz üzere Junit4’te her testten önce çalışmasını istediÄŸniz methodu belirler. Biz her testten önce mock nesnelerimizin yenilenmesini istiyoruz. Çünkü her testte farklı bir davranış bekleyebiliriz. Kimiznde exception bekleriz kiminde doÄŸru sonuç dönmesini bekleriz.
- MocitoAnnotaions.initMocks metodu adından da anlayacağınız üzere, Mockito annotasyonlarının çalışması için kullanılıyor
Genel olarak sınıfın detaylarına girdik. Şimdi de biraz test metodumuzu ineleyelim.
- İlk testimiz başarılı olan bir authentication işlemi üzerine olacak.
- İlk başta test sırasında kullanacağımız User stub nesnemizi yarattık. Normal uygulamada böyle bir nesne büyük ihtimal olmayacak. Ama normal bir nesne olabilecek durumda.
- Mock’lama iÅŸleminin beklentiler üzerine kurulu olduÄŸunu belirtmiÅŸtik. Bu sebepten IUserService sınıfımızın nerede ne zaman ne ÅŸekilde davranacağını belirtiyoruz. EÄŸer stub nesnemiz gelirse, IUserService içerisindeki authentication methodu true yani baÅŸarılı dönüş yapacak.
- LoginController sınıfımızı ilk yaratırken söylemiştik. Eğer authentication başarılı (true) olursa, ana sayfaya yönlendirme yapılacaktı. Bu sebepten LoginController sınıfımızın redirect edeceği sayfayı kontrol ediyor.
- Son olarak Mock’ladığımız nesnenin hangi metodunun kaç kere çaÄŸrıldığını kontrol ediyoruz. Böylelikle baÅŸarılı cevabının doÄŸru nesneden geldiÄŸine emin oluyoruz.
Bu şekilde testimizi çalıştırdığımızda başarılı sonuç alacağız.
Aynı ÅŸekilde Test Coverage bilgisine bakacak olursak, LoginController içerisindeki Methodların %100’ünü test ettiÄŸmizi göreceÄŸiz. Zaten tek bir methodumuz oluduÄŸu için bu beklendik bir durum. Fakat authentication methodunun tamamını kontrol etmedik. Daha kontrol etmemiz gereken iki durum daha var. Birincisi username veritabanında kayıtlı fakat ÅŸifresi yanlış, ikincisi ise kullanıcı veritabanında da kayıtlı deÄŸil. Bu iki durumu daha kontrol etmediÄŸimiz için Test Coverage içerisindeki Line Coverage deÄŸerimiz daha %60. Åžimdi LoginControllerTest sınıfı içerisine yeni bir test adımı daha ekleyip bu deÄŸerimizi yükseltmeye bakalım.
1 2 3 4 5 6 7 8 9 10 11 12 | @Test public void testAuhtenticationForWrongPassword() throws Exception { final User userStub = new User(); userStub.setUsername("bhdrkn"); userStub.setPassword("passowrd"); Mockito.when(userService.authenticate(userStub)).thenReturn(false); final String redirect = loginController.authenticate(userStub); Assert.assertEquals(redirect, "errorPage?message=wrongPassword"); Mockito.verify(userService, new Times(1)).authenticate(Mockito.any(User.class)); } |
Burada yine aynı iÅŸlemi yaptık sadece beklentimizi deÄŸiÅŸtirdik. Az önce aynı ÅŸifreyle login beklentisinde bulunurken ÅŸimdi login olamama beklentisinde bulunuyoruz. Böylelikle kullanıcı login olamadığında doÄŸru sayfaya redirect edildiÄŸini test etmiÅŸ oluyoruz. Fakat bu test Line Coverage bilgimizi sadece %70 yaptı. Gördüğünüz üzere bir yerden sonra Line Coverage’ı arttırmak daha zorlaşıyor.
Şimdi ise son testimizi ekleyelim, normalde büyük ihtimal güvenlik nedeniyle böyle bir bilgi dönmek istemezsiniz. Ama bu tutorial kapsamında eğitici olacağını düşünüyorum.
1 2 3 4 5 6 7 8 9 10 11 12 | @Test public void testAuhtenticationForUserNotFound() throws Exception { final User userStub = new User(); userStub.setUsername("bhdrkn"); userStub.setPassword("passowrd"); Mockito.when(userService.authenticate(userStub)).thenThrow(UserNotFoundException.class); final String redirect = loginController.authenticate(userStub); Assert.assertEquals(redirect, "errorPage?message=userNotFound"); Mockito.verify(userService, new Times(1)).authenticate(Mockito.any(User.class)); } |
Evet burada yine beklentimizi değiştirdik. Az önce true ya da false beklerken şimdi Exception fırlatmasını bekliyoruz. Hemen ardından exception fırlattığında doğru redirect işlemlerini yaptığımıza emin oluyoruz.
Son
Åžimdi Test Coverage’ımızı ölçtüğümüzde, LoginController sınıfında Line Coverage bilgisinin %100’e ulaÅŸtığını görüyoruz. Hiç getter/setter metodumuz olmadığından bu sayıya rahatlıkla ulaÅŸabildik. Ama getter/setter metodlarımız da olsaydı bir kaç test daha yazmamız ya da fazladan Assertion yapmamız gerekebilirdi. Tabi mockito’nun tüm özelliÄŸi bu kadar deÄŸil, baÅŸka bir ton daha özelliÄŸe sahip ama umarım sizin için güzel bir ilk adım olmuÅŸtur.
Kaynak kodlarına aşağıdaki adresten erişebilirsiniz.
End Of Line