Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241202 TDD 적용하고 기능 구현 하기 본문

개발Article

[TIL]20241202 TDD 적용하고 기능 구현 하기

최밤빵 2024. 12. 2. 02:51

이번 클랜플레이어 기능 구현에서 TDD 를 먼저 적용해봤다. 테스트 코드를 먼저 작성하고 그 테스트를 통과하는 방향으로 구현을 진행하고싶었다. 기능 자체는 간단했기에, 테스트를 작성한 후 기능을 호다닥 구현할 수 있었다. 테스트가 명확한 방향을 제공해 주었기에 기능을 구현하는 데 있어 큰 어려움이 없었다! (기능 구현 보다 테스트 코드 작성하는게 더 어렵다. 잘 안해봤던거라 그런가....?)

 

먼저, ClanPlayersService의 주요 기능인 플레이어의 랭크 통계와 라이프타임 통계를 가져오는 메서드를 테스트하기 위한 테스트 케이스들을 정의했다. Mockito를 사용하여 OpenRankedStatsService, OpenLifetimeStatsService, PlayersRepository, RankedStatsRepository, LifetimeStatsRepository와 같은 의존성을 모두 모킹했다. 모킹을 통해 실제 서비스 의존성을 사용하지 않고도 독립적인 테스트 환경을 구축할 수 있었다.


랭크 통계 조회 기능 테스트 (getPlayerRankedStats())

랭크 통계 조회 기능인 getPlayerRankedStats()에 대해 첫 번째로 작성한 테스트는 정상적인 데이터를 제공했을 때 올바른 RankedStatsResponseDto가 반환되는지 확인하는 거였다. openRankedStatsService.getOpenSeasons()의 응답을 모킹해서 실제 데이터를 제공하는 것처럼 테스트를 수행하고, PlayersRepository에서 플레이어 이름을 가져오는 부분도 모킹을 통해 정확한 플레이어 이름이 설정되는지 확인했다. 테스트 결과로는 모든 필드가 정확히 설정되고, 데이터가 저장 또는 업데이트되는 로직까지 검증되었다.

@Test
void shouldReturnRankedStatsWhenValidDataProvided() throws JsonProcessingException {
    // Arrange
    String platform = "pc";
    String playersId = "player123";
    String seasonsId = "season456";

    // Mock the ranked stats response
    RankedStats rankedStats = new RankedStats();
    rankedStats.setData(new RankedData(/* mock attributes */));

    when(openRankedStatsService.getOpenSeasons(platform, playersId, seasonsId))
            .thenReturn(Mono.just(rankedStats));

    when(playersRepository.findByPlayersId(playersId))
            .thenReturn(Optional.of(new Player("player123", "TestPlayer")));

    // Act
    Mono<RankedStatsResponseDto> result = clanPlayersService.getPlayerRankedStats(platform, playersId, seasonsId);

    // Assert
    StepVerifier.create(result)
            .expectNextMatches(dto -> dto.getPlayersName().equals("TestPlayer") &&
                                   dto.getRankedTier().equals(rankedStats.getData().getAttributes().getRankedGameModeStats().getSquad().getCurrentTier().getTier()))
            .verifyComplete();

    // Verify repository save was called
    verify(rankedStatsRepository).save(any(ClanPlayersRankedStats.class));
}

두 번째 테스트 케이스는 잘못된 시즌 ID나 플레이어 ID를 제공했을 때, 적절한 예외가 발생하는지 확인했다. 이 경우 Mono.error(new IllegalArgumentException())가 반환되어야 하는데, 이를 통해 잘못된 입력에 대한 오류 처리가 잘 동작하는지 검증했다.

@Test
void shouldReturnErrorWhenInvalidDataProvided() throws JsonProcessingException {
    // Arrange
    String platform = "pc";
    String playersId = "invalidPlayer";
    String seasonsId = "invalidSeason";

    when(openRankedStatsService.getOpenSeasons(platform, playersId, seasonsId))
            .thenReturn(Mono.just(new RankedStats()));  // Mocking with empty data

    // Act
    Mono<RankedStatsResponseDto> result = clanPlayersService.getPlayerRankedStats(platform, playersId, seasonsId);

    // Assert
    StepVerifier.create(result)
            .expectError(IllegalArgumentException.class)
            .verify();
}

라이프타임 통계 조회 기능 테스트 (getPlayerLifetimeStats())

라이프타임 통계를 가져오는 getPlayerLifetimeStats() 메서드에 대해서도 유사한 방식으로 테스트를 작성했다. 정상적인 데이터를 제공했을 때 필드가 정확히 설정되고 저장 로직이 잘 수행되는지 확인하는 테스트와, 잘못된 데이터를 제공했을 때 예외 처리가 되는지 확인하는 테스트 코드를 작성했다. 각 테스트 케이스들은 모두 예상한 대로 동작하고, 서비스 메서드들이 기대한 대로 구현되었음을 확인했다.

@Test
void shouldReturnLifetimeStatsWhenValidDataProvided() throws JsonProcessingException {
    // Arrange
    String platform = "pc";
    String playersId = "player123";
    String seasonsId = "season456";

    LifetimeStats lifetimeStats = new LifetimeStats();
    lifetimeStats.setData(new LifetimeData(/* mock attributes */));

    when(openLifetimeStatsService.getOpenLifetimeStats(platform, playersId, seasonsId))
            .thenReturn(Mono.just(lifetimeStats));

    when(playersRepository.findByPlayersId(playersId))
            .thenReturn(Optional.of(new Player("player123", "TestPlayer")));

    // Act
    Mono<LifetimeStatsResponseDto> result = clanPlayersService.getPlayerLifetimeStats(platform, playersId, seasonsId);

    // Assert
    StepVerifier.create(result)
            .expectNextMatches(dto -> dto.getPlayersName().equals("TestPlayer") &&
                                   dto.getPublicAvgDamage() > 0)
            .verifyComplete();

    // Verify repository save was called
    verify(lifetimeStatsRepository).save(any(ClanPlayersLifetimeStats.class));
}
@Test
void shouldReturnErrorWhenLifetimeDataInvalid() throws JsonProcessingException {
    // Arrange
    String platform = "pc";
    String playersId = "invalidPlayer";
    String seasonsId = "invalidSeason";

    when(openLifetimeStatsService.getOpenLifetimeStats(platform, playersId, seasonsId))
            .thenReturn(Mono.just(new LifetimeStats()));  // Mocking with empty data

    // Act
    Mono<LifetimeStatsResponseDto> result = clanPlayersService.getPlayerLifetimeStats(platform, playersId, seasonsId);

    // Assert
    StepVerifier.create(result)
            .expectError(IllegalArgumentException.class)
            .verify();
}

TDD 적용 결과 & 느낀점

TDD를 적용한 덕분에 구현 과정에서 명확한 목표를 가지고 진행할 수 있었다. 기능 자체는 어렵지 않았기에 테스트가 요구하는 조건을 만족시키는 방향으로 코드를 작성할 수 있었다. 테스트가 요구하는 조건을 만족시키는 방향으로만 코드를 작성하니 불필요한 로직 없이 간결하고 명확한 코드를 작성할 수 있었다. 또한, 테스트를 미리 작성해두었기 때문에 이후 리팩토링 과정에서도 코드가 의도한 대로 동작하는지 쉽게 확인할 수 있었다. 이런 간단한 기능에 TDD를 통해 설계하고 검증하는게 맞나 하는 생각이 들었지만 한 코드 품질이나 유지보수성을 높이는 중요한 경험이 될 수 있을거란 생각에 적용하는 연습을 했다. TDD를 적용하는 연습은 복잡한 문제를 다룰 때만 유용한 게 아니라, 작은 기능에서부터 습관화하는게 코드의 일관성과 안정성을 확보하는 데 큰 도움을 준다. 이렇게 작은 기능에서부터 TDD를 적용하면 실제 복잡한 기능을 다룰 때도 자연스럽게 테스트 기반의 개발 습관을 유지 할 수 있을거 같다.