Spring Boot ile Unit Test Yazmak

Betül Şahin
7 min readFeb 10, 2022

Bu yazıda Spring Boot uygulamalarında unit test yazmayı konuşacağız. Başlangıçta test kavramına temel bir giriş yapıyor olacağız. Konudan bağımsız olarak unit testlerin nasıl yazıldığına bakacağız. Test yazımı bakış açısını anlamaya çalışacağız. Ardından ise Controller ve Service sınıflarına nasıl birim testi yazıldığını inceleyeceğiz.

Bu yaz öğrenci olarak katılmış olduğum GittiGidiyorKodluyoruz & Patika.dev işbirliğiyle düzenlenen GittiGidiyor Java Spring Framework Bootcamp’inde öğrenmiş olduklarımı derledim. Bizlerle bilgi ve tecrübelerini içtenlikle paylaşan eğitmenimiz Koray Güney’e teşekkür ediyorum.

Unit Test Nedir ?

Unit test yazmak önemli bir konu ve uygulamayı geliştiren kişinin sorumluluğunda.

Wikipedia ’dan unit test tanımına bakalım.

Birim testi, yazılım programlamasında bir tasarım ve geliştirme yöntemidir. Bu yöntemde yazılımcı yazılım kodunu oluşturan birimlerin kullanıma hazır olduğuna iknâ olur. Birim, bir bilgisayar uygulamasında test edilebilecek en küçük bölüme denir.

Farklı test tipleri var. Unit test ve entegrasyon testi bunlardan bazıları.

Test Metodu Nasıl Yazılır ?

Testlerimizi src/test klasörü altına yazıyoruz. Directory yapısı bizi bu konuda yönlendiriyor.

Default olarak gelen test sınıfını silip SampleUnitTestClass sınıfını oluşturuyoruz.

İlk olarak hiçbir şey yazmadan aşağıdaki gibi bir test metodu oluşturalım.

  • void — Test metodları her zaman void olarak yazılır. Çünkü test metodlarını herhangi bir yerde çağırıp kullanmıyoruz. Ayrıca bu tüm birim testlerin birbirinden bağımsız olduğu anlamına geliyor.
  • @Test — Test metodlarımızın başına her zaman bu anotasyonu koyuyoruz. org.junit.jupiter.api paketinde bulunuyor. JUnit kütüphanesi Spring içerisinde default olarak gelmektedir ve unit test yazabilmemizi sağlamaktadır. Java 8 ile beraber JUnit 5 yayınlandı.

Testimizi koşturmak için metodun solunda bulunan run butonuna tıklıyoruz. Aşağıdaki çıktıyı alıyor olacaksınız. Sağ alt bölümde test sonucu yer alıyor.

Haydi başlayalım ! 💪

SampleUnitTestClass sınıfına Calculator inner sınıfını oluşturuyoruz. Ve içerisine iki sayının toplamını döndüren add() metodunu yazıyoruz.

Şimdi bu sınıf için test_add() metodunu yazalım. Test metodlarımızı BDD(Behaviour driven development) yaklaşımıyla yazıyoruz. Bu yaklaşımı given, when ve then bölümleri olarak söyleyebiliriz.

  • given — bu senaryoda test edeceğimiz davranışa başlamadan önce değişkenlerin değerleri. Bunu testin ön koşulları olarak düşünebilirsiniz.
  • when — test edeceğimiz davranış. Yani test edeceğimiz metodu bu bölümde çalıştırıyoruz.
  • then — test edeceğimiz davranış nedeniyle beklediklerimiz. Test ettiğimiz metoddan hangi sonuçları beklediğimizi burada yazıyoruz.

Test metodunda, Calculator sınıfının add(a, b) metoduna 10 ve 20 parametrelerini girdiğimizde 30 sonucunun dönmesini beklediğimizi söyledik. Testi run ettiğinizde aşağıdaki sonucu alacaksınız.

Her şey yeşil olduğuna göre testimiz geçti !

Expected değerini farklı bir değer vererek testin fail durumunu gözlemleyebilirsiniz. Ben burada yazıyı uzatıp vaktinizi almamak adına eklemedim.

Unit Testlerde İsimlendirme

Bu konuda farklı isimlendirme tipleri bulunuyor. Bu yazıda bootcamp eğitiminde öğrendiğim isimlendirme yöntemini aktaracağım.

@Test
public void should_returnEquals_when_addTwoNumber(){
}

Burada should ve when prefixlerimiz. Yani bunları her zaman koyuyoruz. Diğer kısımlar ise metodumuza göre değişiyor. Test metodunu okuduğumuzda, o testin ne için yapıldığını çıkarabilmeliyiz. Metodun daha kolay okunması adına alt tireler kullanıyoruz.

Test metodumuzu şu şekilde okuyabiliriz: İki sayı birbirine eklendiğinde bunlar eşit olmalı.

Worst Case İçin Test Yazmak

Yukarıda best case senaryosu için test metodu geliştirdik. Bunun tersi durumla da karşılaşabiliriz. İki sayı toplandığında birbirine eşit olmadığı durumları da test etmemiz gerekiyor.

Şimdi aynı örnek için olumsuz durumu yazalım.

Çalıştırdığınızda yine testin geçtiğini göreceksiniz. Burada test metodumuza 10 ve 20 sayılarının toplamının 40'a eşit olmamasını beklediğimizi söyledik. Nitekim 40'a eşit değil ve testimiz geçti.

Bazı Temel Noktalar

  • Test sınıfının içindeki tüm testleri aynı anda koşmak isterseniz sınıfın solundaki run komutunu çalıştırmalısınız.
  • DisplayName() — Metod ismini değiştirmek için kullanılıyor.
@Test
@DisplayName(value = "This test should return equals when add two number")
public void should_returnEquals_when_addTwoNumber(){

...
}
  • RepeatedTest() — Aynı testi tekrarlı olarak girilen parametre değeri kadar koşar.
@Test
@RepeatedTest(10)
public void should_returnNotEquals_when_addTwoNumber(){
}
  • ParameterizedTest()Aynı testi, birden fazla değer ile koşturmak için kullanılıyor.

Örnek üzerinden incelemek için yeni bir test metodu yazıyoruz. Bir sayıyı 0 ile çarptığımızda sonucunun 0 dönmesini bekliyoruz.

Yukarıda değerlerimizi ValueSource() ile girdik. Bununla beraber birden fazla değer girmek için CsvSource() anotasyonu kullanılıyor.

Hata Durumlarının Test Edilmesi

Bunun için Executable kullanılıyor. JUnit 5 ile beraber ile gelen güzel bir özellik. Executable interface i içerisine hatayı sarmallıyoruz.

Yukarıdaki örnek kodun son satırını(then), yorum satırına alarak fırlattığı hatayı görebilirsiniz. “java.lang.ArithmeticException: / by zero” hatasını alıyor olacaktık. Executable ile bu hatayı sarmalladık ve assertThrows ile fırlatıldığını kontrol ettik.

assertThrows()’un birinci parametresine test edeceğimiz hata türünü yazıyoruz.

  • BeforeEach — Her test metodundan önce çalışır. Bir değişkenin initialize edilmesi gibi durumları burada yapabiliriz.

Burada calculatorTest nesnemizi oluşturma işlemini setup() metodunda gerçekleştirdik.

  • AfterEach — Her bir test sonrası yapılacak işlemler için kullanılıyor. Çok sık kullanılmayan bir anotasyon.
  • BeforeAll — Tüm testlerden önce koşulur. Statik olarak tanımlanır. Örneğin data source ve dosya işlemleri gibi.
  • AfterAll — Tüm testlerden sonra kaynakları boşaltmak için kullanılıyor. Statik olarak tanımlanır. Bu iki anotasyon özellikle entegrasyon testleri için kullanışlı olabilir.

Controller ve Service Sınıflarına Birim Testi Yazmak

Kodlarımızı yazarken ne kadar çok şişkin metod olursa (yani mümkün olduğunca az satırda işi bitirmekten öte çok uzun implementasyona sahip bir metodumuz olursa) onun unit testini yazmak zorlaşır. Ne kadar minimal metodlar olursa unit testini yazmak kolaylaşır.

Controller ve Service testlerine geçmeden önce çok kısa TDD yani test driven development konusuna değinmek istiyorum. Bu yaklaşımda ilk olarak testi ardından kodu yazıyoruz. Bu yazıda amacım nasıl unit test yazıldığına genel hatlarıyla değinmek. Bu yüzden örnek çalışmada TDD yaklaşımını kullanmayacağım. Sadece bilgi vermek amaçlı bahsetmek istedim. Daha detaylı bilgi için şu ve şu videoları izlemenizi tavsiye ederim.

Case

localhost:8080/api/v1/customers adresinde yayınladığımız bir restfull apimiz var. Müşteri ekleme, listeleme ve silme işlemlerini gerçekleştiriyor.

Controller Testi

CustomerController sınıfı içerisindeyken Ctrl+Shift+T ‘ye basın. src/test/ controllers klasörü altına CustomerControllerTest sınıfı oluşacak. Test metodlarımızı bu sınıfın içerisine yazıyor olacağız.

create() Metodu Best Case İçin Test

İlk olarak create metodu için test yazalım. Dikkat ettiyseniz bu metod içerisinde dışarıdan CustomerService sınıfını kullanıyoruz. Bu bizim için bir zorluk anlamına geliyor. Çünkü birim testinde dışarıdaki her şeyden bağımsız sadece metodumuzun doğru şekilde çalışıp çalışmadığına bakıyoruz.

Bu noktada yapmamız gereken servise doğrudan gitmeden customerService.create(request) ile dönen değeri taklit etmek yani mocklamak. Bunun içinde Spring’in içerisinde hazır gelen Mockito’yu kullanacağız.

  • @ExtendWith(MockitoExtension.class) — Test sınıfları için kullanılan anotasyon. (Mockito’yu kullanacağımızı söyledik.)
  • @MockTest edeceğimiz sınıfın bağımlılıklarını mocklamak yani taklit etmek için kullanılan anotasyon.
  • @InjectMocksMock nesnelerini test edeceğimiz sınıfa inject etmek için kullanılıyor.
  • @Test Test metodları için kullanılan anotasyon.

Artık testimizi yazabiliriz ! 💪

Aşağıdaki kod satırında(8. satır), test metodumuza mockCustomerService.create() çağrıldığında expected değişkenini dönmesini söylüyoruz. Burada mock işlemimizi gerçekleştirmiş olduk.

  • when() Hangi metodun çağrılacağını burada belirtiyoruz.
  • thenReturn() — Çağrılan metodun hangi sonucunu dönmesini beklediğimizi burada belirtiyoruz.
  • any() Obje tipinde herhangi bir parametre alabileceğini belirttik.
when(mockCustomerService.create(any())).thenReturn(expected);

11–13. satırlarda create metodumuzu test ediyoruz ve dönen sonucu actual değişkenine atıyoruz.

16. satırdan itibaren ise dönen ve beklediğimiz sonuçları karşılaştırıyoruz.

assertAll(
() -> assertNotNull(actual),
() -> assertEquals(HttpStatus.CREATED, response.getStatusCode()),
() -> assertEquals(expected.get(), actual),
() -> assertEquals(customer.getIdentificationNumber(), actual.getIdentificationNumber()));
  • assertNotNull(actual) — Assert iddia etmek anlamına geliyor. Burada parametre olarak geçtiğimiz actual değişkeninin null olmayacağını iddia ediyoruz. Eğer actual değişkeni null değilse true dönecektir.
  • assertEquals(param1, param2) — Her iki parametrenin birbirine eşit olmasını karşılaştırıyoruz.

Soldaki yeşil run butonuna tıklayıp çalıştıralım. Testimiz geçti!

create() Metodu Worst Case İçin Test

Şimdi de create metodunun hata aldığı durumu test edelim. Hatırlarsanız controller’da mockCustomerService.create() metodundan null dönmesi durumunda HttpStatus.BAD_REQUEST döndürüyorduk. Bunu test etmek için 4. satırda thenReturn()’e null değere sahip Optional nesnesi geçtik.

getAll() ve getById() Metodlarının Test Edilmesi

Bu metodlar için yine aynı işlemler tekrar ettiği için burada yer vermedim. Şu repodan tamamına ulaşabilirsiniz.

deleteById() Metodunun Test Edilmesi

Yine bu metod için de standart işlemler tekrarlanıyor. Burada worst case’e yer vermek istedim.

Controller sınfına gidelim. Müşteri varsa silip return değerimiz void olduğu için HttpStatus.NO_CONTENT döndürüyoruz. Müşteri bulunamadıysa HttpStatus.NOT_FOUND döndürüyoruz.

Şimdi müşteri bulunamadı durumu için aşağıdaki test metodunu yazalım.

4. satırı inceleyelim.

  • doThrow(param) — buraya fırlatılacak hata sınıfını belirtiyoruz.
  • when(param).deleteById(anyLong()) — burada servis nesnemizin deleteById() metodunu çağıracağımızı belirtiyoruz. Dikkat ettiyseniz öncekinden farklı olarak nokta ile ayırarak yazdık. Bunun sebebi metodun void dönmesi.

Service Testi

Servis testlerinde de benzer işlemler yapılmaktadır. Dilerseniz aşağıdaki repodan inceleyebilirsiniz.

--

--