← Kursa Dön
📄 Text · 28 min

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:

  1. Repository ve diğer bağımlılıkları mock et

  2. Service'i mock'larla oluştur

  3. İş mantığını farklı senaryolarla test et

  4. 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_expectedResultcreateUser_WithDuplicateEmail_ShouldThrow
should_expectedBehavior_when_conditionshouldThrow_WhenEmailDuplicate
given_when_thengivenDuplicateEmail_whenCreateUser_thenThrowException

Sık Yapılan Hatalar

  1. Test'te iş mantığı tekrarlama: Service'deki hesaplamayı test'e kopyalamayın — beklenen sonucu sabit değer olarak yazın

  2. Her şeyi mock'lama: DTO'lar, value object'ler gerçek nesne olarak kullanılmalı — sadece dış bağımlılıkları mock'layın

  3. 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)

  4. Arrange bölümünün şişmesi: @BeforeEach veya 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.