Java Dilinde Test Yazmak
Test yazmak yazılımlarımızdaki en küçük işlem parçalarının beklenen yetenekleri sergileyip sergilemediğini kontrol etmemizi sağlayan yöntemdir.
Basitçe, test yazmayı öğrenip uygulamalarımızda kullanabilmeyi bu yazı ile amaçlıyoruz.
- Junit https://junit.org
- Hamcrest http://hamcrest.org
- AssertJ http://joel-costigliola.github.io/assertj/
- Mockito https://mockito.org
Java dilinde yazdığımız kodları test etmek için kullanabileceğimiz bir çatıyı öğrenmeye çalışalım.
JUnit Framework
JUnit, Java dili ile geliştirilen uygulamalarda kullanabileceğimiz yazılımda en küçük birimleri test etmemizi sağlayan bir kütüphanedir.
- https://junit.org/
JUnit testlerinin amacı Java sınıfını ve sahip olduğu tüm bağımlılıkları test etmek değildir. JUnit testlerinde Java sınıfları izole edilmiş olarak düşünülür ve sınıfların işlevleri test edilir.
Bir örnek ile ilerleyelim.
- https://github.com/junit-team/junit4/wiki/Getting-started#create-a-test
Bir sınıfımız var ve içerisinde bir metot var.
public class Calculator {
public int evaluate(String expression) {
int sum = 0;
for (String summand: expression.split("\\+"))
sum += Integer.valueOf(summand);
return sum;
}
}
Sınıfımızın içindeki metodun işlevini test etmek için başka bir sınıf oluşturuyoruz.
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class CalculatorTest {
@Test
public void evaluatesExpression() {
Calculator calculator = new Calculator();
int sum = calculator.evaluate("1+2+3");
assertEquals(6, sum);
}
}
Test eden metodu belirtmek için metodumuz üzerine @Test ifadesi ile belirtiyoruz.
Bu ifadeden başka test sınıfları içinde metotlarda kullanılacak başka ifadeler mevcuttur.
@BeforeClass sınıf için bir kez ve ilk olarak çalışır
@Before her test metodudan önce
@Test test metodunun kendisindir
@After her test metodundan sonra
@AfterClass sınıf için bir kez ve en son çalışır
Junit Assertion
Yazdığımız test metodumuzun içinde o metottan beklediğimiz durumu kontrol etmek isteriz. O metot içindeki iş mantıklarını parçalayarak sonuçlarını test etmek isteriz.
- https://github.com/junit-team/junit4/wiki/Assertions
Kullanılması çok tercih edilenlere bakalım.
assertEquals() --> karşılaştırılan nesne örneğini içlerindeki equals() metodu ile test eder.
assertSame() --> karşılaştırılan nesne örneğini içlerindeki equals() metodu kullanmasan test eder.
assertNull() --> parantez içindeki değerin null olmasını bekler
assertNotNull() --> parantez içindeki değerin null olmamasını bekler
assertTrue() --> parantez içindeki değerin true olmasını bekler
assertFalse() --> parantez içindeki değerin false olmasını bekler
assertArrayEquals() --> parametre olarak verilen dizi karşılaştırılır
Junit Parameter
Bir test metodunu farklı test parametreleri ile tekrar terkra test etmek isteyebiliriz. Bu durumda aynı metodun farklı veri parametresi alan hallerini yazmak yerine şu kütüphaneden yararlanabiliriz.
-
http://pragmatists.github.io/JUnitParams
-
https://github.com/Pragmatists/JUnitParams
@Parameters("a","b","c") yazımı ile metoda gönderilerek parametreleri tek noktadan yönetebiliyoruz.
Junit Exception
Bir işlem içerisinden istediğimiz bir istisna (exception) durumunu oluşturup test edebiliriz.
Örneğin listede olmayan bir değeri çağırdığımızda bu durumu anlamak ve yönetmek istiyoruz.
- https://github.com/junit-team/junit4/wiki/Exception-testing#expectedexception-rule
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
List<Object> list = new ArrayList<Object>();
thrown.expect(IndexOutOfBoundsException.class);
thrown.expectMessage("Index: 0, Size: 0");
list.get(0); // execution will never get past this line
}
Junit Suite
Yazdığımız testlerin tek noktadan çalıştırılmaya başlamasını sağlayabiliriz.
@Suite ifadesi ile birden fazla test sınıfını koşturabiliriz.
@RunWith(Suite.class)
@SuiteClasses({ UnitTest.class, SeviceTest.class })
public class AllTestsForProduct {
}
@Ignore ifadesi yazılmış fakat çalıştırılmasını istemediğimiz test metotlarının işaretlenerek çalışmasını engeller. Devre dışı bırakır
@Ignore(value=" Bu metot örnek olsun diye çalıştırılmadı.")
@Test
public void testPrintMessage() {
System.out.println("hello");
}
Hamcrest
Birçok dildeki test sınıfları içindeki metotlara yardımcı olarak daha okunabilir hallerini yazabilmemize yardım eder.
- http://hamcrest.org/
Bir eşleştirme,karşılaştırma kütüphanesidir.
Hamcrest kütüphane olarak projenize eklenip kullanabiliriz. Test metotlarımızı yazarken sadece Hamcrest e ait olan metotları kullarak yazabiliriz ya da JUnit ile birlikte harmanlayarak kullanabiliriz.
Örneğin
Elimizdeki liste üzerinde işlemler yaparken,
Assert.assertThat() ile JUnit ten , CoreMatchers.hasItem() ile Hamcrest ten yararlanabiliriz.
@Test
public void testNumber() {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.forEach(item -> System.out.println(item.intValue()));
Assert.assertThat(list, CoreMatchers.hasItem(1));
Assert.assertThat(list, CoreMatchers.hasItem(2));
}
@Test
public void testNumberString() {
List<String> list = new ArrayList<>();
list.add("bir");
list.add("iki");
list.forEach(item -> System.out.println(item));
Assert.assertThat(list.get(0), CoreMatchers.startsWith("b"));
Assert.assertThat(list.get(1), CoreMatchers.is("iki"));
Assert.assertThat(list.get(1), CoreMatchers.containsString("k"));
}
Kullanılması çok tercih edilen metotlara bakalım
equalTo() --> Eşitlik kontrolü
containsString() --> Beklenen değer içinde istenilen değer var mı kontrolü.
anyOf() --> or durumudur. içindeki parametrelerin herhangi birinin doğruluğunda doğru sonuç verir.
allOf() --> and durumur. içindeki parameterlerinin hepsinin doğru olmasını bekler.
either().or() --> bu veya bu karşılaştırması için kullanılır.
hasItem() --> liste karşılaştırılmalarında içerisinde bir elemanın olup olmadığı kontrolünü yapar
AssertJ
Test metotlarına yardımcı olacak kütüphanelerden biridir. Metotlar içindeki beklenen değerin üzerine kontroller ekleyebilmemizi sağlar.
Örneğin
Bir cümle üzerindeki bazı kontrolleri görelim.
@Test
public void testSampleValue() {
Assertions.assertThat("sample value")
.startsWith("sa")
.endsWith("e")
.containsOnlyOnce("v");
}
Bir başka güzel yanı ise istediğimiz durumları metotlar haline getirip tekrar tekrar kullanabiliriz.
Örneğin
Elimizdeki listenin tek sayıları olmasını bekleyen bir metot içeriyor.
Eğer liste içerisinde çift olan bir sayı var ise uyarı veriyoruz.
@Test
public void testEvenOddNumbers() {
List<Integer> sayilar1 = new ArrayList<>(Arrays.asList(1,3,5));
List<Integer> sayilar2 = new ArrayList<>(Arrays.asList(1,3,4));
Assertions.assertThat(sayilar1)
.describedAs("UYARI - Hata olustugunda bu uyariyi veriyorum - Cift sayi var")
.have(oddNumber());
Assertions.assertThat(sayilar1)
.describedAs("UYARI - Hata olustugunda bu uyariyi veriyorum - Liste içinde 3 tane tek sayı bulunamadı")
.haveExactly(3,oddNumber());
Assertions.assertThat(sayilar2)
.describedAs("UYARI - Hata olustugunda bu uyariyi veriyorum - Cift sayi var")
.have(oddNumber());
Assertions.assertThat(sayilar2)
.describedAs("UYARI - Hata olustugunda bu uyariyi veriyorum - Liste içinde 3 tane tek sayı bulunamadı")
.haveExactly(3,oddNumber());
}
private Condition<? super Integer> oddNumber() {
return new Condition<Integer>() {
@Override
public boolean matches(Integer value) {
if(value % 2 == 1 ) {
System.out.println("tek sayı");
return true;
}
return false;
}
};
}
Projelerimizdeki sınıflarımızın özellikleri kontrol edebilmemizi sağlayan metotları vardır.
public class Product {
private String name;
private int type;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
@Test
public void testListValue() {
Product p1 = new Product();
p1.setType(1);
p1.setName("product1");
Product p2 = new Product();
p2.setType(2);
p2.setName("product2");
List<Product> products = new ArrayList<>();
products.add(p1);
products.add(p2);
Assertions.assertThat(Product.class).hasDeclaredFields("name");
Assertions.assertThat(Product.class).hasDeclaredFields("type");
Assertions.assertThat(Product.class).hasOnlyDeclaredFields("name","type");
Assertions.assertThat(products)
.extracting("name", "type")
.contains(
Assertions.tuple("product1",1),
Assertions.tuple("product2",2)
);
}
hasDeclaredFields() --> metodu ile sınıf içindeki alanlarını tek tek kontrol edebiliriz.
hasOnlyDeclaredFields() --> metodu ile sınıf içindeki tüm alanları tek metot ile kontrol edebiliriz.
Bu metot tüm alanların eklenmesini beklemektedir. Sınıfa yeni bir alan eklendiğinde,metoda da
eklenmesini bekleyeceğinden hata vererek eksik test yazılmasının önüne geçer :)
Assertions.tuple() --> metodu ile sınıf içindeki alanlara karşılık gelmesini istediğimiz değerleri
tek tek kontrol edebilmemizi sağlıyor
AssertJ ye ait özellikleri Assertions sınıfı ile kullanabiliyoruz.
Behavior Driven Development yaklaşımı ile yazmak istersek BDDAssertions isimli sınıfı kullanabiliriz.
Mock
Mock taklit, sahte anlamına gelmektedir.
Testlerimizi yazarken aynı zamanda gerçek veriler ile test etmek isteyebiliriz. Fakat elimizde her zaman test edebileceğimiz veri olmayabilir. Gerçek verileri taklit eden veriler üreterek testlerimizin doğruluğunu kontrol edebiliriz.
JUnit testlerinde mock nesneler sıkça kullanılır. Mock nesneler kullanılarak test edilen Java sınıfının bağımlılıkları test esnasında varmış gibi taklit edilir. Test edilen Java sınıfı kullanılan Mock ve gerçek sınıflar arasında ayrım yapamaz. Bu yüzden nasıl bağımlı olduğu sınıflarla beraber çalışıyorsa, test esnasında da bağımlılıklarını temsil eden mock nesneler ile işlem yapılır.
Java yazarken kullanabileceğimiz mock kütüphanelerine bir kaç örnek verelim
-
Mockito https://site.mockito.org
-
EasyMock http://easymock.org
-
PowerMock https://github.com/powermock/powermock
-
Jmock http://jmock.org
Mockito kütüphanesini inceyelerek devam edeceğiz
Mockito
Test yazarken bir işlemin çalışabildiğini kontrol etme ihtiyacımız olduğunda faydalanarabiliriz.
Temel mantık ,
yazılımdan yapmasını beklediğimiz işlemi karşılayan sahte veri veya işlem üretip, yazılımdan yapmasını beklediğimiz işlemi çalıştırıp sahte veri veya işlem ile karşılaştırmaktır.
Bu durum yazılımın hangi işlemleri kapsadığını açıkça ortaya koymaktadır.
Mockito Stubbing
Test verilerini beklediğimiz aksiyonu yazarken belirleyebiliriz. Peki buna neden ihtiyaç var ?
Service isimli interface ve içinde gövdesi olmayan bir metot var. ServiceImpl isimli sınıf ise Service interface implement ettiğinden içindeki metot kullanılabilr durumdadır.
Metot çağrıldığında beklediğimiz durumu kontrol etmek, test etmek istiyoruz. Fakat elimizde o metodun runtime da çalıştığında üreteceği çıktısı yok. Test edebilmek için metodun bulunduğu sınıfın aynısından ismi stub ile biten ServiceImplStub bir sınıf oluşturup Service interface implement ediyoruz. İçleri boş haldeki metotlardan dönülmesini istediğimiz değerleri stub sınıfımızdaki metotlara yazıyoruz.
Service interface içinde bir değişiklik olduğunda otomatik olarak ServiceImplStub sınıfı etkilendiğinden istediğimiz gibi ekleme çıkarma yapmayıp, test verisini oluşturup test metotlarını yazarak eklemek istediğimiz metodu Service interface e eklemiş oluyoruz.
Her zaman önce test yazma işlemini yapmaya özen gösteriyoruz. Daha sonra test sınıflarımız içinde sadece bu stub isimli sınıfımızdaki metotlardan dönen durumları kontrol ederek test yazmaya çalışıyoruz. Bu çok zahmetli ve uzun olabiliyor.
Bu durumun yerine
Mockito veya benzer kütüphaneler kullanarak given .. when .. thenReturn gibi ifadeler yardımıyla
dinamik olarak istediğimiz kadar tekrar edebilen yapılar kurabiliyoruz.
Konu olarak yeri gelmişken Martin Fowler şöyle bir makale yayınlamış.
- https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs
Şöyle bir ifadesi var.
There is a difference in that the stub uses state verification while the mock uses behavior verification.
Bu konuda aklım biraz karıştı. Test tarafında bilgili arkadaşlar görüşlerini paylaşırlarsa sevinirim.
Örneğimize geçelim Burada listeye bir eleman eklemek yerine stub oluşturarak beklenen durumu oluşturdum. Aslında bunu kural yazmak gibi düşünebiliriz.
Listenin ilk elemanı çağrıldığında 30 değeri dönecek şeklinde bir kural yazdım. Daha sonra kurala uyan bir işlem yaptım ve bunu konsola yazdırdım. Test işlemini bu şekilde doğrulayabildik.
Bu şekilde test yapmanın bize sağladığı kolaylık mock ile belirlemiş objelerin içerisindeki değerlerin ne olduğu ile ilgilenmiyoruz. Sadece beklediğimizi kısımdaki kuralı yazarak test etmeyi amaçlamış oluyoruz.
@Test
public void testMethodStubbing () {
List<Integer> list = Mockito.mock(ArrayList.class);
list.add(30);
// stubbing
Mockito.when(list.get(0)).thenReturn(30);
System.out.println(list.get(0));
}
// https://static.javadoc.io/org.mockito/mockito-core/2.23.4/org/mockito/Mockito.html#stubbing
Mockito Verification
Örneğin
Mockito kütüphanesi kullanarak listemizi sahte nesne olacak şekilde oluşturuyoruz. Sahte nesneye özellikler kazandırmak için listeye elemanlar ekleyip daha sonra boyutunu kontrol ediyoruz.
Yaptığımız işlemleri doğrulama ihtiyacı şu aşamada bize yardımcı oluyor. Gerçekte bu elemanları ekleyebilecek miydim?
Listenin bir metodunu kullandık.Burada hazır metot kullandık fakat kendi yazdığımız bir metotta bu yöntemle test edebiliriz. Son satırda listemize 30 sayısını ekleyip eklemediğimizi kontrol ediyoruz. Bu işlem yapılmadığı için bu satır bize uyarı vererek bu işlemin daha önceki satırlarda yapılmadığının garantisini sağlamış oluyor.
@Test
public void testMockitoMethod () {
// mocking
List<Integer> list = Mockito.mock(ArrayList.class);
list.add(10);
list.add(20);
list.size();
// verification
Mockito.verify(list).size();
Mockito.verify(list).add(10);
Mockito.verify(list).add(20);
Mockito.verify(list).add(30); // error
}
Mockito Order Verification
Test etmek istediğimiz durumların sırasıyla çalışmasını isteyebiliriz. Sıralı yapılan işlemelerin kontrolü için bu durumu kullanabiliriz.
InOrder inOrder = inOrder(firstMock, secondMock);
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
// https://static.javadoc.io/org.mockito/mockito-core/2.23.4/org/mockito/Mockito.html#in_order_verification
Mockito VerifyNoMoreIteraction
Test ettiğimiz durumdan sonra bu metot içerisinde başka duruma izin vermek istemediğimizi belirtiyoruz.
Mock işlemi uygulanan listeye bir eleman eklenmiş ve bu eleman verify edilmiş durumda işte bu işlemden sonra liste üzerinde farklı bir işlem bu metot kapsamında yapılamasın istiyorsak verifyNoMoreInteractions kullanarak bu durumu oluşturabiliriz. Eğer yorum işareti kaldırılıp listeye ikinci bir eleman eklenirse bu test hata verecektir çünkü yeni bir işlem yapılamasın demiştik.
Kod içerisinde given bölümü
@Test
public void testMethodNoMoreElement () {
List<String> mockedList = Mockito.mock(ArrayList.class);
mockedList.add("one");
//mockedList.add("two");
Mockito.verify(mockedList).add("one");
Mockito.verifyNoMoreInteractions(mockedList);
}
// https://static.javadoc.io/org.mockito/mockito-core/2.23.4/org/mockito/Mockito.html#finding_redundant_invocations
Mockito VerifyZeroIteraction
Belirsemde belirtmesemde bu mock nesnesi bu metotta hiç bir zaman çağrılamasın.
Mockito.verifyZeroIteractions(resultList);
https://static.javadoc.io/org.mockito/mockito-core/2.23.4/org/mockito/Mockito.html#never_verification
Mockito Behavior
Basitçe bir örnek ile konuyu özetleyelim. Testlerimizi davranış belirterek yazabiliriz. given.. when .. then.. şeklinde yazılan bir durum belirtiyoruz. Bunu Mockito ile yaptığımız için alt satırlarda when kısmına yazılan davranısı sergilediğimizde yanı bunu yapan bir kod yazdığımızda then kısmındaki işlem,dönüş değeri olarak sağlanır. ve daha sonra mock objelerimizi test ederek test metodunun amacını gerçekleştirmiş oluruz
Given bölümünde dönüş değeri olarak Güzel Kitap bekleniyor.
Test çalıştırıldığında
When bölümünde resul olarak Güzel Kitap dönecektir.
Then bölümünde karşılaştırma işlemi yapılarak işlem tamamlanacaktır.
public class ProductServiceImpl {
public String getProductByCode(String string) {
return "Güzel Kitap";
}
}
@Test
public void testBDDStyle() {
ProductServiceImpl productService = Mockito.mock(ProductServiceImpl.class);
// given
BDDMockito.given(productService.getProductByCode(Mockito.eq("kitap"))).willReturn("Güzel Kitap");
// when
String result = productService.getProductByCode("kitap");
// then
Assertions.assertThat(result).isEqualTo("Güzel Kitap");
}
yay back