Service Unit Testing
Service katmanı, uygulamanızın iş mantığının yaşadığı yerdir. Controller HTTP isteğini alır ve service'e iletir; repository veritabanı ile konuşur. Service katmanı bu ikisi arasında durur ve iş kurallarını uygular. Bu katmanı test etmek, uygulamanızın doğru çalıştığından emin olmanın en kritik adımıdır.
Service Test Pattern
Service testlerinde genel kalıp şudur:
Repository ve diğer bağımlılıkları mock et
Service'i mock'larla oluştur
İş mantığını farklı senaryolarla test et
Repository çağrılarını verify et
Gerçekçi Bir Örnek: UserService
Önce test edeceğimiz service sınıfını görelim:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
public UserDto createUser(CreateUserRequest request) {
// İş kuralı 1: Email benzersiz olmalı
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException(
"Email already registered: " + request.getEmail());
}
// İş kuralı 2: Şifre encode edilmeli
User user = User.builder()
.name(request.getName())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER)
.active(true)
.build();
User saved = userRepository.save(user);
// İş kuralı 3: Hoş geldiniz emaili gönderilmeli
emailService.sendWelcomeEmail(saved.getEmail(), saved.getName());
return UserDto.fromEntity(saved);
}
public UserDto getUserById(Long id) {
return userRepository.findById(id)
.map(UserDto::fromEntity)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
public UserDto updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
user.setName(request.getName());
if (request.getEmail() != null
&& !request.getEmail().equals(user.getEmail())) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException("Email already in use");
}
user.setEmail(request.getEmail());
}
return UserDto.fromEntity(userRepository.save(user));
}
public void deactivateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
user.setActive(false);
userRepository.save(user);
}
}Kapsamlı Test Sınıfı
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock private UserRepository userRepository;
@Mock private PasswordEncoder passwordEncoder;
@Mock private EmailService emailService;
@InjectMocks private UserService userService;
private CreateUserRequest validRequest;
private User savedUser;
@BeforeEach
void setUp() {
validRequest = CreateUserRequest.builder()
.name("Ali Yılmaz")
.email("ali@example.com")
.password("securePass123")
.build();
savedUser = User.builder()
.id(1L)
.name("Ali Yılmaz")
.email("ali@example.com")
.password("encodedPassword")
.role(Role.USER)
.active(true)
.build();
}
// ─── createUser Testleri ─────────────────────────────────────
@Test
@DisplayName("Geçerli request ile kullanıcı oluşturulmalı")
void createUser_WithValidRequest_ShouldCreateUser() {
// Arrange
when(userRepository.existsByEmail("ali@example.com")).thenReturn(false);
when(passwordEncoder.encode("securePass123")).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// Act
UserDto result = userService.createUser(validRequest);
// Assert
assertAll(
() -> assertEquals("Ali Yılmaz", result.getName()),
() -> assertEquals("ali@example.com", result.getEmail()),
() -> assertNotNull(result)
);
// Verify — tüm bağımlılıklar doğru çağrıldı mı?
verify(userRepository).existsByEmail("ali@example.com");
verify(passwordEncoder).encode("securePass123");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("ali@example.com", "Ali Yılmaz");
}
@Test
@DisplayName("Var olan email ile DuplicateEmailException fırlatılmalı")
void createUser_WithDuplicateEmail_ShouldThrowException() {
when(userRepository.existsByEmail("ali@example.com")).thenReturn(true);
DuplicateEmailException ex = assertThrows(
DuplicateEmailException.class,
() -> userService.createUser(validRequest)
);
assertTrue(ex.getMessage().contains("ali@example.com"));
verify(userRepository, never()).save(any()); // save çağrılMAMALI
verify(emailService, never()).sendWelcomeEmail(any(), any());
}
@Test
@DisplayName("Şifre encode edilmiş olarak kaydedilmeli")
void createUser_ShouldEncodePassword() {
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(passwordEncoder.encode("securePass123")).thenReturn("$2a$10$encoded");
when(userRepository.save(any(User.class))).thenReturn(savedUser);
userService.createUser(validRequest);
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
assertEquals("$2a$10$encoded", userCaptor.getValue().getPassword());
}
// ─── getUserById Testleri ────────────────────────────────────
@Test
@DisplayName("Var olan kullanıcı ID ile bulunmalı")
void getUserById_WhenUserExists_ShouldReturnUser() {
when(userRepository.findById(1L)).thenReturn(Optional.of(savedUser));
UserDto result = userService.getUserById(1L);
assertEquals("Ali Yılmaz", result.getName());
verify(userRepository).findById(1L);
}
@Test
@DisplayName("Olmayan kullanıcı ID ile UserNotFoundException fırlatılmalı")
void getUserById_WhenUserNotFound_ShouldThrowException() {
when(userRepository.findById(999L)).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class,
() -> userService.getUserById(999L));
}
// ─── deactivateUser Testleri ─────────────────────────────────
@Test
@DisplayName("Kullanıcı deaktive edilmeli")
void deactivateUser_ShouldSetActiveToFalse() {
when(userRepository.findById(1L)).thenReturn(Optional.of(savedUser));
when(userRepository.save(any(User.class))).thenReturn(savedUser);
userService.deactivateUser(1L);
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(captor.capture());
assertFalse(captor.getValue().isActive());
}
}Test İsimlendirme Konvansiyonları
İyi test isimleri, test raporu okunduğunda neyin test edildiğini anlatır:
| Format | Örnek |
|---|---|
methodName_scenario_expectedResult | createUser_WithDuplicateEmail_ShouldThrow |
should_expectedBehavior_when_condition | shouldThrow_WhenEmailDuplicate |
given_when_then | givenDuplicateEmail_whenCreateUser_thenThrowException |
Sık Yapılan Hatalar
Test'te iş mantığı tekrarlama: Service'deki hesaplamayı test'e kopyalamayın — beklenen sonucu sabit değer olarak yazın
Her şeyi mock'lama: DTO'lar, value object'ler gerçek nesne olarak kullanılmalı — sadece dış bağımlılıkları mock'layın
verify() aşırı kullanımı: Sonucu assertion ile doğrulayın, implementasyon detayını verify ile kontrol etmeyin (kırılgan testlere yol açar)
Arrange bölümünün şişmesi:
@BeforeEachveya test fixture builder ile ortak setup'ı çıkarın
Service katmanı testleri, uygulama güvenilirliğinin bel kemiğidir. Repository'leri mock'layarak iş mantığını izole bir şekilde test etmek, en yüksek ROI'ye sahip test türüdür.
AI Asistan
Sorularını yanıtlamaya hazır