Spring Boot Uygulamalarında Test Stratejileri: Gerçek Hayat Deneyimleri ve En İyi Pratikler

Agit Rubar Demir
4 min readOct 21, 2024

--

Yazılım geliştirme sürecinde testler, projenin başarısı için hayati öneme sahiptir. Özellikle Spring Boot gibi güçlü bir framework ile çalışırken, doğru test stratejileri uygulamak, uygulamamızın güvenilirliğini ve sürdürülebilirliğini artırır. Bu yazıda, gerçek hayat deneyimlerimizden yola çıkarak Spring Boot uygulamalarında test stratejilerinin nasıl uygulandığını ve bunların pratikte nasıl fayda sağladığını inceleyeceğiz.

Neden Test Yazmalıyız?

Test yazmak, birçok geliştirici tarafından zaman kaybı olarak görülebilir.

Ancak uzun vadede düşünüldüğünde, iyi yazılmış testler:
1. Hataları erken aşamada yakalamamızı sağlar.
2. Kodun güvenilirliğini artırır.
3. Refactoring süreçlerini kolaylaştırır.
4. Dokümantasyon görevi görür.
5. Geliştirme sürecini hızlandırır.

Kısa vadede zaman kaybı gibi görünen testler, aslında uzun vadede büyük bir zaman kazancı sağlar.

Test Tipleri

1. Unit Test (Birim Testi)

Unit testler, kodumuzun en küçük parçalarını (genellikle metodları) izole bir şekilde test etmemizi sağlar.

Karmaşık iş mantığı içeren servis katmanları için idealdir. Hızlı çalışır ve spesifik fonksiyonaliteyi test eder.

Özellikler:
Genellikle service veya spesifik uygulama sınıfları için yazılır (örn. UserServiceImpl, UserPersistenceAdapter).
Mockito gibi araçlar kullanılarak bağımlılıklar mocklanır.
Sadece test edilen metodun davranışına odaklanılır.
Çok hızlı çalışır ve sık sık çalıştırılabilir.

Avantajlar:
Kodun belirli bir parçasının doğru çalıştığından emin olunur.
Hataları hızlı bir şekilde izole etmeyi sağlar.
Refactoring sırasında güven verir.

Dezavantajlar:
Bileşenler arasındaki etkileşimleri test etmez.
Aşırı mocklama, gerçek davranıştan uzaklaşmaya neden olabilir.

Pratik İpuçları:
Her bir test metodu için tek bir senaryo test edin.
Edge case’leri ve hata senaryolarını da test etmeyi unutmayın.
Test yazarken kod yazarmış gibi düşünmeyip, ortaklaştırma yapmamaya çalışın. Testleriniz açık bir şekilde yazılmalı ve okunabilir olmalıdır.

Kod Örneği:

@Test
void givenValidId_whenUserFoundById_thenReturnUser() {

// Given
Institution mockInstitution = new InstitutionBuilder()
.withValidValues()
.build();
Mockito.when(identity.getInstitutionId())
.thenReturn(mockInstitution.getId());

AysUser mockUser = new AysUserBuilder()
.withValidValues()
.withInstitution(mockInstitution)
.build();
String mockId = mockUser.getId();

// When
Mockito.when(userReadPort.findById(mockId))
.thenReturn(Optional.of(mockUser));

// Then
AysUser user = userReadService.findById(mockId);

Assertions.assertNotNull(user);
Assertions.assertEquals(mockUser, user);

// Verify
Mockito.verify(identity, Mockito.times(1))
.getInstitutionId();

Mockito.verify(userReadPort, Mockito.times(1))
.findById(Mockito.anyString());
}

https://github.com/afet-yonetim-sistemi/ays-be/blob/bfaaa8fdb3f205b4f4d87ad90e3c018abf0c2b35/src/test/java/org/ays/auth/service/impl/AysUserReadServiceImplTest.java#L115

2. Integration Test (Entegrasyon Testi)

Integration testler, farklı bileşenlerin birlikte nasıl çalıştığını kontrol eder.

Controller’ların doğru çalıştığını, veritabanı işlemlerinin beklendiği gibi gerçekleştiğini veya harici servislerle iletişimin düzgün kurulduğunu kontrol etmek için kullanılır.

Özellikler:
Controller, repository veya Kafka, Redis gibi dış bağımlılıklar için yazılabilir.
MockMvc kullanılarak HTTP istekleri simüle edilir.
Bağımlılıklar Mockito ile mocklenebilir, ancak gerçek bağımlılıkları kullanmak da mümkündür.
Uygulamanın çalışmasını gerektirdiği için unit testlere kıyasla daha yavaş çalışır, ancak daha gerçekçi senaryoları test eder.

Avantajlar:
Bileşenlerin birlikte doğru çalıştığını doğrular.
API sözleşmelerinin doğru uygulandığını kontrol eder.
Veritabanı işlemlerini gerçek bir ortamda test edebilir.

Dezavantajlar:
Unit testlere göre daha yavaş çalışır.
Hataların kaynağını belirlemek daha zor olabilir.

Pratik İpuçları:
Test veritabanı olarak H2 gibi in-memory veritabanları kullanın.
Her testten önce veritabanını temizleyin veya sıfırlayın.
Gerçek HTTP isteklerini simüle etmek için MockMvc kullanın.
API’nizin farklı HTTP metodlarını (GET, POST, PUT, DELETE, etc.) test edin.
Hata durumlarını ve sınır koşullarını da test etmeyi unutmayın.

Kod Örneği:

@Test
void givenValidUserId_whenUserFound_thenReturnAysUserResponse() throws Exception {

// Given
String mockUserId = AysRandomUtil.generateUUID();

// When
AysUser mockUser = new AysUserBuilder()
.withValidValues()
.withId(mockUserId)
.build();

Mockito.when(userReadService.findById(mockUserId))
.thenReturn(mockUser);

// Then
String endpoint = BASE_PATH.concat("/user/").concat(mockUserId);
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.get(endpoint, mockAdminToken.getAccessToken());

AysUserResponse mockUserResponse = userToResponseMapper
.map(mockUser);
AysResponse<AysUserResponse> mockResponse = AysResponse
.successOf(mockUserResponse);

aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
.andExpect(AysMockResultMatchersBuilders.status()
.isOk())
.andExpect(AysMockResultMatchersBuilders.response()
.isNotEmpty());

// Verify
Mockito.verify(userReadService, Mockito.times(1))
.findById(mockUserId);
}

https://github.com/afet-yonetim-sistemi/ays-be/blob/bfaaa8fdb3f205b4f4d87ad90e3c018abf0c2b35/src/test/java/org/ays/auth/controller/AysUserControllerTest.java#L272

3. End-to-End Test (Uçtan Uca Test)

End-to-End testler, uygulamanın tüm bileşenlerinin birlikte çalışmasını uçtan uca test eder. User Acceptance Test olarak da bilinir.

Kullanıcı senaryolarını tam olarak test etmek ve sistemin bir bütün olarak çalıştığından emin olmak için kullanılır.

Özellikler:
Gerçek bir kullanıcı senaryosunu simüle eder.
Tüm katmanlar ve bağımlılıklar gerçek ortamda test edilir.
Testcontainers gibi araçlar kullanılarak gerçekçi ortamlar oluşturulabilir.
En yavaş çalışan, ancak en kapsamlı test tipidir.

Avantajlar:
Uygulamanın gerçek dünya senaryolarında nasıl davrandığını gösterir.
Tüm sistem bileşenlerinin entegrasyonunu doğrular.
Kullanıcı deneyimini simüle eder.

Dezavantajlar:
Çalıştırması zaman alır ve kaynak yoğundur.
Hataların kaynağını belirlemek zor olabilir.
Bakımı diğer test tiplerine göre daha zordur.

Pratik İpuçları:
Uçtan uca testlerin gerçekleştirilebilmesi için H2 yerine Testcontainers kullanarak gerçek veritabanı ve diğer bağımlılıkları (örn. MySQL, Redis, Kafka) simüle edin.
Tam bir kullanıcı akışını test edin (CRUD operasyonları).
Test verilerini önceden hazırlayın ve her testten sonra temizleyin.
Asenkron işlemleri test ederken uygun bekleme mekanizmaları kullanın.
Performans sorunlarını tespit etmek için zamanlama ölçümleri ekleyin.

Kod Örneği:

@Test
void givenValidUserId_whenUserExists_thenReturnUserResponse() throws Exception {

// Initialize
Institution institution = new InstitutionBuilder()
.withId(AysValidTestData.Admin.INSTITUTION_ID)
.build();

AysRole role = roleReadPort.findAllActivesByInstitutionId(institution.getId())
.stream()
.findFirst()
.orElseThrow();

AysUser user = userSavePort.save(
new AysUserBuilder()
.withValidValues()
.withoutId()
.withRoles(List.of(role))
.withInstitution(institution)
.build()
);

// Given
String userId = user.getId();

// Then
String endpoint = BASE_PATH.concat("/user/").concat(userId);
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.get(endpoint, adminToken.getAccessToken());

AysUserResponse mockUserResponse = userToResponseMapper
.map(user);

AysResponse<AysUserResponse> mockResponse = AysResponse
.successOf(mockUserResponse);

aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
.andExpect(AysMockResultMatchersBuilders.status()
.isOk())
.andExpect(AysMockResultMatchersBuilders.response()
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("id")
.value(user.getId()))
.andExpect(AysMockResultMatchersBuilders.response("emailAddress")
.value(user.getEmailAddress()))
.andExpect(AysMockResultMatchersBuilders.response("firstName")
.value(user.getFirstName()))
.andExpect(AysMockResultMatchersBuilders.response("lastName")
.value(user.getLastName()))
.andExpect(AysMockResultMatchersBuilders.response("phoneNumber.countryCode")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("phoneNumber.lineNumber")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("city")
.value(user.getCity()))
.andExpect(AysMockResultMatchersBuilders.response("status")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("roles[*].id")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("roles[*].name")
.isNotEmpty())
.andExpect(AysMockResultMatchersBuilders.response("createdUser")
.value(user.getCreatedUser()))
.andExpect(AysMockResultMatchersBuilders.response("createdAt")
.isNotEmpty());
}

https://github.com/afet-yonetim-sistemi/ays-be/blob/bfaaa8fdb3f205b4f4d87ad90e3c018abf0c2b35/src/test/java/org/ays/auth/controller/AysUserEndToEndTest.java#L183

Sonuç

Spring Boot uygulamalarında farklı test tiplerini kullanmak, uygulamamızın güvenilirliğini ve kalitesini artırır. Her bir test tipi, farklı senaryoları ve kullanım durumlarını kapsar. Başlangıçta zaman alıcı görünse de, uzun vadede bu testler geliştirme sürecini hızlandırır, hataları azaltır ve kodun sürdürülebilirliğini artırır.

Unutmayın, iyi bir test stratejisi, projenizin başarısı için kritik öneme sahiptir. Test yazmayı ihmal etmeyin ve bu farklı test tiplerini projenizin ihtiyaçlarına göre dengeli bir şekilde kullanın.

Gerçek hayatta kullanımları için, aşağıdaki açık kaynak repoyu inceleyebilirsiniz;
https://github.com/afet-yonetim-sistemi/ays-be/tree/main/src/test/java/org/ays

--

--

Agit Rubar Demir
Agit Rubar Demir

Written by Agit Rubar Demir

Software Engineer at Finartz | Technical Mentor & Consultant, Project Management Consultant

No responses yet