코드 품질을 위하여..

현재 프로젝트에서 사용하고 있는 기술스택은 ... 이러한 이유로 테스트 코드를 작성하기로 결심했다. 어떤 테스트 코드를 작성해야할까..? 결론은 slice 테스트가 유일한 길이었다. 현재 service, controller 레이어만 테스트를 작성한 상태 service -> Mock controller -> Mock mapper -> 통합테스트를 적용하여 개발 db에 붙여보기 도전중..

Mock, Spy.. 그리고

Dao 테스트

현재 Mybatis 중에서도 dao 방식으로 데이터에 접근하도록 구축되어 있다. 하지만 여러 의존성 이슈..

MSA로 구성되었다.. 하지만 모노리식, 멀티모듈 구조라는 표현이 더 적합하다고 생각한다.

메인 모듈에서 모든 설정과 의존성 주입된다. 서브모듈을 라이브러리 형식으로 main애서 implement하여 사용한다. 이러한 문제로 config나 주입을 하기위해 여러가지를 고민해야 했다.

  1. 데이터에 접근하는 공통 dao 설정만 따로 분리 -> 시도중
  2. main 모듈을 testImplement하여 사용 -> 실패

목차

테스트 코드를 작성하며 겪게된 여래 시행착오와 배웠던 점을 간단하게 정리해보려고 한다. 본 프로젝트에서 사용하는 스택과 구조에 대하여 간단하게 설명을 하고 테스트에 대하여 알아보자.

  • Jdk 17.0.14
  • Junit 5.10.5
  • Mybatis3.0.3
  • Spring-boot 3.3.8 (>canalFrame)

위 스펙을 사용하여 멀티모듈 구조로 이루어져 있다. 예를 들어 A, B, C라는 도메인들이 있고 하나의 도메인 A에서 B, C를 implement하여 사용하고 있는 구조이다. A -> B, C

즉, 모든 설정에 관련된 정보는 메인 모듈 A에 의존적이다. 이 구조로 SpringBootApplication이 A 모듈에만 존재하기 때문에 B, C 모듈에서는 이 의존성을 주입해줄 방법이 없었다.

더 쉽게 말해 datasource를 통하여 DB Connection을 찾아가야 하는데 datasource에 관련된 설정은 A 모듈에만 존재하기 때문에 B, C 모듈에서는 새로운 설정을 할 필요했다.


테스트 방식 선정 단계

테스트를 작성하기 전에 Mock사용하여 단위테스트를 진행할지 통합 테스트를 하는것이 좋을까?

이 고민은 초기에 Application Context에 Bean을 등록할 수 없는 문제로 종결되었다. 단위테스트를 하기로 마음을 먹었으며, 단위 테스트를 하게 될때 장점은 아래와 같다.

단위테스트는 단순한 오류를 찾는 것이 아니다. 각각의 컴포넌트, 즉 단위 별로 독립적으로 잘 동작을 하는지 검증하는 것이다. 각각의 단위를 조립 했을때 유기적으로 코드가 흘러가는 것을 가정하며 진행한다.

단위테스트 VS. 통합테스트

단위테스트

  • 하나의 컴포넌트를 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트
  • 컴포넌트는 하나의 기능 또는 메소드
  • 어떤 기능이 실행되면 어떤 결과가 나오도록 테스트하는 것

통합테스트

  • 모듈을 통합하는 과정에서 모듈 간의 호환성을 확인하는 테스트
    • A service를 호출하는 B Service와 각각 database에서 값을 읽어와 상호작용하여 기댓값을 잘 봔환하는지 확인
  • 모든 통합된 컴포넌트들이 연계되어 동작하는지 검증
단위테스트의 문제점

구조적으로 1개의 애플리케이션은 또 다른 1개의 객체와 메시지를 주고 받아야한다. 앞서 한개의 컴포넌트 단위로 테스트를 독립적으로 테스트 해야하기 때문에 다른 객체를 호출할때 문제가 있을 수 있다. 이러한 경우 Stubbing을 통하여 문제를 해결할 수 있다.


JUnit

given-when-then

강의나 대부분의 실무에서 해당 패턴을 활용하여 테스트 코드를 작성한다.

패턴 given - 어떤 데이터가 준비되었을때 - 준비 when - 테스트할 컴포넌트를 실행하면 - 실행 then - 어떠한 결과가 나올것 - 검증 해당 패턴을 활용하여 개발을 진행했으며 간혹 verify를 통하여 호출 횟수를 검증하기도 했다.

예시코드

@Test
@DisplayName("01_판매번호_종합조회_기본조회")
void searchSalesNo() {
    // given
    SalesNoLtgrSttusResDto expected =
            SalesNoLtgrSttusResDto.builder()
                    .coCd(CO_CD)
                    .outwhSalNo(SAL_NO)
                    .build();


    given(userContext.getCoCd()).willReturn(CO_CD);
    given(commonDao.selectList("salesNoLtgrSttusService.searchSalesNo", reqDto)).willReturn(List.of(expected));
    //when

    when(salesNoLtgrSttusService.searchSalesNo(reqDto)).thenReturn(List.of(expected));

    //then
    assertThat(salesNoLtgrSttusService.searchSalesNo(reqDto)).isEqualTo(List.of(expected));
    verify(commonDao).selectList("salesNoLtgrSttusService.searchSalesNo", reqDto);
}

테스트를 위하여 필요한 함수가 별도로 있으면 테스트 코드 내부에 선언하여 사용해도 괜찮다. spring-test와 junit-jupiter 각각의 역할을 제대로 구분짓고 가보려고 한다.



JUnit 5와 Spring의 역할 구분
JUnit 5 (junit-jupiter)
  • 테스트 프레임워크
  • 테스트 실행생명주기 관리
Spring (@SpringBootTest,@ContextConfiguration)
  • 애플리케이션 컨텍스트 관리
  • 의존성 주입테스트 컨텍스트 초기화

이 두 역할을 명확히 구분할 필요가 있다.

Test Double(테스트 대역)

위 세가지 모두 테스트에서 의존성이나 외부 객체 동작을 제어하거나 검증하기 위해 사용하는 Test Double(테스트 대역)이다.

  • 테스트 대역은 실제 객체를 대체하지만, 완전히 동일한 기능을 제공하는 것이 아닌 특정 목적에 맞게 동작을 변경하는 것이다.
종류 실제 객체 대신하는 방식
Stub 고정된 값을 반환하여 출력 제어
Mock 메서드 호출 여부 검증
Spy 실제 객체 + 일부 동작 조작

Stub

고정된 값을 반환하거나 미리 정의된 동작을 수행하는 테스트 대역이다

  • 고정된 값을 반환하여 예측 가능한 값을 반환하도록 동작을 미리 정의한다.
  • 동작만 제공하며 검증을 하지 않는다.
  • 상태 기반의 테스트에 적합하기 때문에 테스트 결과가 특정 상태와 일치하는지 확인할 때 사용한다.

Mock

mock이란 '모의, 가짜'란 의미이다. 테스트할 때 필요한 실제 객체와 동일한 가짜 객체를 만들어 사용하는 객체로 Mock Object가 있다.

  • 행위 기반 테스트에 적합하며 호출 여부와 호출 패턴을 검증한다.
  • 테스트 중 동적으로 행동을 정의할 수 있다.
  • Mockito 라이브러리에서 기능 제공

Spy

Spy는 실제 객체를 감싸면서 일부 메서드만 Stub처럼 동작하도록 구현한다.

  • 실제 동작과 스텁 동작 혼합 가능
  • 부분적인 검증이나 메서드 조작에 용이