Spring - 테스트코드 심화편 (Mock, Stub)

김상엽·2024년 4월 23일
0

Spring

목록 보기
26/26
post-thumbnail

TIL

  • 최종프로젝트 28일차, 이제 슬슬 마무리 단계에 돌입했다.
  • 테스트 코드 작성을 맡으면서 만난 문제와 해결을 하여서 기록하려고 한다.

@Mock 객체와 가설(Stub) 세우기

  • Mock 객체는 가짜로 등록되는 객체이기 때문에 해당 객체를 이용하여 하는 모든것에 대해 미리 결과를 정의해줘야한다.
  • 위 코드에서도 60번째 줄에 categoryRepository.saveTEST_CATEGORY1을 반환할 것이다라고 미리 정의해줬다.
    • any()는 "어떤게 들어오던" 같은 의미이며, 조금 더 세분화 하여 anyString(), anyList()등등 형식을 지정할 수 있다.
  • 가설을 세워 줬다면, 테스트 하려는 부분을 실행하고, verifyassertEquals등으로 검증한다.
    • 위의 코드에서는 verify를 통해 categoryRepositorysave가 1번일어났는지 검사하고,
    • 결과값과 요청값을 비교하였다.
  • 테스트코드의 전반적인 구성인 이렇게 된다. (가설설정 -> 테스트 -> 검증)

가설(Stub) 세우면서 만난 문제들

  • 테스트코드의 전반적인 구성을 알아보았고, 다음은 구성중 하나인
    가설을 설정하는 부분에서 만난 문제들이다.
  • 전반적으로 새롭게 알게된 중요한 점은 이거였다.

가설 설정에는 가설 순서도 영향을 미친다.

UnnecessaryStubbingException

  • 가설을 세웠지만 사용하지 않는 가설일 경우 발생하는 에러이다.
  • 처음에는 가설을 Mock 객체가 채워주지 못하는 부분을 설정해주는 것이라고만 생각했는데,
    생각보다 빡세게 적용되었다. (기본 설정이 strict stubbing)
  • 해결 방법으로는 두가지가 있다.
      1. 사용하지 않는 가설을 제거한다.
      1. Loose stubbing으로 변경한다. (lenient)

Loose Stubbing (lenient)

  • 63번째 코드처럼 Mockito.lenient.when(가설 상황).thenReturn(반환값)처럼 코드를 작성하면, 가설이 필요하면 사용하고, 필요없다면 그냥 넘어간다. (Loose stubbing)

RedisTemplate NullPointerException

  • 테스트를 하려는 입찰 관련 코드에는 RedisTemplate.opsForValue().get()을 통해 값을 가져오는 부분이 있다.
  • 그래서 이 부분도 가설을 설정해주었는데, 에러가 발생했다.

  • NullPointerException이 발생하였다. 원인은 RedisTemplate.opsForValue().get()을 하기 위해서는 RedisTemplate.opsForValue()값이 있어야 하는데 이부분이 Null이어서 발생했다.
  • 그래서 위 코드처럼 RedisTemplate.opsForValue()값을 가설로 세워주어 해결했다고 생각했는데,
    테스트를 시행할때마다 전부 통과하거나, 일부만 통과하는 증상이 생겼다.
  • 전혀 이해가 되지 않는 상황이어서 원인이 뭘까 정말 많은 시행착오를 겪었다..
    • 모두 동일한 가설에서 결과비교만 다르게 구현해놓았는데 일부만 통과했었다.
      (심지어 매번 통과하는 테스트가 달라짐)
  • 찾아낸 원인이 확실하진 않지만, 전에 블로그를 찾아보다가 GitActions의 Redis 설정에 대해 찾아보고 적용해놓은 적이 있었는데, 이를 제거했더니 해결되었다. Embedded-redis 블로그
  • gradle에서 embedded-redis를 제거했더니 해결되었다. 아무래도 이게 원인이 아니었을까..
  • 개인적으로 코딩을 시작한 이후 가장 이해가 되지 않는 오류였다.

테스트 결과

  • 구현한 28개의 테스트 모두 통과하는것을 확인하며 매우 희열을 느꼈다.
  • 테스트 결과를 보기 좋게 묶는 방법도 찾아보고 적용하였다.
  • @TestClassOrder(ClassOrderer.OrderAnnotation.class)를 통해 @Nested클래스들의 순서를 적용하고,
  • @Nested클래스에는 @TestMethodOrder(MethodOrderer.OrderAnnotation.class)를 통해 테스트들의 순서를 적용해주었다.
  • 한가지 아쉬운점은 @Nested클래스에 @DisplayName을 통해 숫자도 같이 작성했는데,
    숫자는 적용이 되지 않는다고 한다.
  • GithubActions를 통해 CI를 하기위해 test.yml도 작성해주었다.
  • 성공적으로 테스트가 완료되었는데, 한가지 아쉬운점이 있었다.
  • Task: test에 테스트 몇개 중 몇개가 통과했는지가 나오지 않았다. (실패하면 몇개중 몇개 실패했는지는 나옴)
  • 그래서 인터넷을 찾아본 결과 테스트 결과를 출력하는 방법을 찾아서 적용해보았다.
tasks.named('test'){
    useJUnitPlatform()
    testLogging {
        afterSuite { testDescriptor, testResult ->
            if (testDescriptor.parent == null) {
                println "Results: ${testResult.resultType} (${testResult.testCount} tests, ${testResult.successfulTestCount} successes, ${testResult.failedTestCount} failures, ${testResult.skippedTestCount} skipped)"
            }
        }
    }
}
  • build.gradle에 해당 코드를 추가하면 끝이다!
  • 테스트 결과와 몇개의 테스트중 몇개를 통과했는지 잘 나온다!

오늘의 회고

테스트 코드를 작성하면서 프로젝트 전반적인 모든 코드들에 대해 이해할 수 있어서
좋은 경험이었다.
다만 작성하면서 든 생각은 코드를 먼저 구현하고 테스트 코드를 구현하다보니
이미 구현된 코드에 테스트코드를 맞추는 느낌이 조금 들었다.
TDD(테스트코드 기반 개발방법)도 기회가 된다면 시도 해보는것도 좋을 것 같다.
진짜 지금 생각해도 NullPointerException문제는 정말 이해가 되지 않는다..
어떻게 빌드할때마다 결과가 다르게 나올 수 있었을까
일단은 기존 Redis와 Embedded-Redis간의 충돌로 그렇지 않았을까 라는 결론을 내렸다.
이제 테스트 코드 작성도 끝났고, 배포와 발표만 남은 상황이다.
끝까지 최선을 다해야겠다!

profile
개발하는 기록자

0개의 댓글