티스토리 뷰

반응형

최근 ORM 없이 순수 JDBC + Spring, 그리고 JDBC template을 통한 SQL mapper + Spring 조합의 웹앱들을 만들어보았다.

Presentation, Service, Persistence 세 레이어를 ATDD 스타일로 구현하며 단위 테스트와 통합 테스트를 다양하게 작성해보았고, 괜찮았던 방법과 고민했던 포인트들을 적어보려고 한다.

 


 

각 레이어에 대한 단위 테스트는 Mockito를 통한 Slice 테스트로

 

Out - In 방식의 TDD에 용이한 Mockito

 

Persistence -> Service -> Presentation 순서로의 In-Out 방식의 개발을 진행하면 상위 레이어를 구현할 때 하위 레이어가 이미 모두 구현되있으므로 하위 레이어를 포함한 단위 테스트를 구현할 수 있다.

 

하지만 도메인이 명확하지 않고 외부의 유저 인터페이스부터 개발하는 방향이 용이할 시,

Presentation -> Service -> Persistence로의 Out - In 방식의 개발이 적절하며, 나 또한 이러한 방식으로 접근을 많이 하였다.

 

이 경우 단점은 상위 계층을 만들 때 하위 계층의 개발이 완료되지 않았다는 것인데,

여기서 Mockito를 사용하면 이런 단점을 상쇄할 수 있다.

 

@WebMvcTest(controllers = TargetController.class)
class ControllerTest {

    @MockBean
    private DependentService dependentService;
    
    @Test
    void testMethod() {
        // 테스트 메서드에서 특정 요청에 대해 기대하는 응답을 stubbing하여 사용한다.
    	// BDD Mockito가 조금 더 가독성이 좋다.
        // 서비스 클래스의 특정 메서드 껍데기만 만들어 두어도 상위 레이어의 테스트가 가능해진다.
        BDDMockito.given(dependentService.method(any(Request.class))).willReturn(response);
        . . .
    }
}

 

 

Stubbing을 통한 깔끔한 슬라이스 테스트, 각 레이어의 단위테스트 조합

 

위에서 언급한 Mockito를 사용하면 의존적인 하위 계층 클래스를 껍데기만 만들어놓은 상태에서 상위 계층부터 만들어갈 수 있다. 나는 각각의 레이어를 아래와 같은 기능을 조합하여 테스트를 진행했다.

  • persistence는 JdbcTest를 통해 인메모리 H2 및 db 관련 컴포넌트만 로드하여 사용한다.
  • service는 mockito를 사용해 dao 혹은 repository를 stubbing하여 검증한다.
  • presentation은 @WebMvcTest를 붙여 HTTP 요청을 모의로 보내는 mockMVC를 활용해 검증한다.

 

@JdbcTest
@Sql(scripts = "classpath:schema-truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
class LineDaoTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private DataSource dataSource;

    private DaoClass daoClass;

    @BeforeEach
    void setUp() {
        this.lineDao = new LineDao(jdbcTemplate, dataSource);
    }

    @Test
    void test() {
        . . .
    }
}

 

@JdbcTest를 통해 JdbcTemplate, DataSource에 대한 의존성은 주입이 가능하도록 하고, 테스트 DB는 별도의 스크립트를 통해 초기화하는 방법이 적절했다.

 

    @Test
    void test() throws Exception {
        // given
        // jackson.databind의 objectMapper를 통해 객체를 json형태로 만든다.
        String jsonRequest = objectMapper.writeValueAsString(request);
        . . .
        
        // when & then
        // json형태의 응답을 후처리하여 필요한 것이 있는지 검증한다.
        mockMvc.perform(post("/api")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(jsonRequest))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("id").value(1L))
            .andExpect(jsonPath("name").value("이름"));
        . . .
    }

 

Presentation Layer의 테스트에 쓰인 mockMvc의 경우 요청과 응답이 json으로 이루어지는 Rest API의 경우 각각을 변환하거나 jsonPath()를 통해 찾아내는 별도의 문법들을 익힐 필요가 있음은 아쉬웠다.

 

 

테스트는 빠른 피드백을 받을 수 있는 것이 가장 좋다. 또한 테스트가 쌓였을 때 실행 속도 또한 지나치게 길어져선 안 된다. 위의 방법을 사용하면 불필요한 Bean 초기화를 최소화하며 테스트들을 수행할 수 있다.

 

 

단위 테스트에서 다른 계층을 mocking한다면 동작을 정말 보장할 수 있을까?

 

물론 서비스 계층의 A라는 기능을 위해 영속성 계층의 B라는 기능을 stubbing하여 테스트를 진행하는 것이 완벽하게 A 기능을 검증한다고 볼 순 없다. 하지만 이것은 영속성 계층의 B 기능을 영속성 계층을 설계할 때 충분히 단위 테스트로 검증한다면 A 기능은 B 기능에 의존하므로 잘 동작할 것으로 예상 가능하다.

따라서 mocking을 통한 단위 테스트는 mocking을 통해 검증되지 않은 부분들을 다른 계층의 단위테스트에서 보완해주어야 한다는 점에서 불편할 수 있으나, 다른 계층의 설계가 끝나지 않은 상태에서 Out - In TDD를 할 수 있다는 점과 기능들을 더 세분화하여 테스트한다는 점에서 강점이 있다.

 


 

통합 테스트는 아래의 두 가지 방법을 적절히

 

웹앱이 간단하여 외부 API 등을 끌어다 쓰는 경우는 없었다. 계층 간 기능들이 통합적으로 동작하는지 검증하는 것으로 충분했다. Cucumber와 같은 도구도 과한 선택이었다.

 

 

@DirtiesContext 는 너무 느리다

 

@DirtiesContext를 클래스 레벨에 적용하면 각각의 테스트 메서드의 실행 이후 컨텍스트를 매번 무효화시킨다.

 

여기서 DB와 관련된 컨텍스트는 DB와의 상호작용과 관련된 빈, 커넥션 등의 정보를 포함한다. DB 테이블과 그 데이터까지 컨텍스트에 포함되는 것은 아니다. 따라서 테스트를 연쇄적으로 진행하였을 때, 컨텍스트 캐싱을 계속 사용한다면 DB의 데이터는 누적되어 테스트의 격리성을 보장할 수 없다.

인메모리 DB를 통해 테스트를 수행하는 경우 @DirtiesContext를 사용하면 테스트 컨텍스트를 새로 로드할 때 DB까지 재구성하므로 테스트 간 격리성을 보장할 수 있다.

 

하지만 DirtiesContext는 매 테스트 메서드마다 모든 컨텍스트와 DB를 재구성하게 되므로 격리성에서는 확실하나 전체적인 테스트의 실행이 너무 느려 비효율적이다.

 

 

SpringBootTest(WebEnvironment = RANDOM_PORT) + RestAssured

 

처음엔 SpringBootTest(WebEnvironment = RANDOM_PORT) + RestAssured를 사용했다.

웹 계층을 포함한 스프링과 관련된 설정을 초기화하고 RestAssured로 실제 Http 요청을 보내 검증하는 방식이다.

 

이 경우 테스트용 DB 컨텍스트는 하나만 공유되어 테스트 간 격리가 불가능했다. 이를 위해 @Sql 을 사용, 별도의 스크립트를 적어두고 테스트 메서드가 실행되기 이전 실행시키는 방식으로 DB 테이블들을 초기화시켜야 했다.

 

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Sql(scripts = "classpath:schema-truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public class IntegrationTest {

    // 각각의 테스트 메서드 이전에 schema-truncate.sql이 실행된다.
    // 해당 스크립트에는 DB 테이블들이 truncate되도록 지정해두었다.
	. . .
}
ALTER TABLE table1
    ALTER COLUMN id RESTART WITH 1;
TRUNCATE TABLE table1;
. . .

 

왜 Random_port에서는 transactional을 못 쓰나?

 

궁금해서 찾아보았을 때 springboot와 관련 공식 문서에서 답을 찾았었다.

random_port 혹은 defined port에서는 테스트의 격리를 위해 HTTP 요청을 보내는 클라이언트와 서버의 쓰레드를 달리한다. 따라서 롤백이 불가능하다.

 

 

쓰레드가 다른 것이 롤백과 무슨 상관인가?

 

롤백은 동일한 커넥션 객체를 사용하는 상황에서만 처리가 가능하다.

추가로 JDBC와 관련한 데이터소스 관련 레퍼런스를 찾아보았는데 thread-bound하게 커넥션을 사용한다. 쓰레드마다 Connection 객체들을 독립적으로 관리하고, 또 각 쓰레드는 여러 Connection들의 풀을 가진다. 하나의 쓰레드가 동시에 여러 트랜잭션을 관리할 수 있는 이유가 이것이다. 멀티쓰레드로 동시에 여러 DB 테스트를 할 수 있을 것 같다고 생각했다.

관련: https://jaehee329.tistory.com/31

 

 

SpringBootTest + AutoConfigureMockMvc @Transactional 붙이기

 

팀원과 논의하며 아래의 방향이 외부 API를 의존하지 않는 현재 웹앱에서 더 빠르고 적절하다는 결론을 내렸다.

물론 외부 환경까지 의존하는 웹앱이라면 RestAssured 등의 도구를 사용하는 것이 맞다.

 

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class IntegrationTest {
	. . .
}

 

기본적으로 SpringBootTest의 WebEnvironment 옵션은 mock.

이 옵션은 서블릿 컨테이너, 톰캣을 띄우지 않는다. 따라서 더 빠르고 가볍다.

여기에 AutoConfigureMockMvc를 사용하면 MockMvc를 사용할 수 있게 된다.

 

MockMvc를 통해 가상의 HTTP 요청과 응답을 제공해 컨트롤러의 동작을 검사할 수 있다. Presentation Layer의 단위 테스트와 방식이 유사해보일 수 있으나, 하위 레이어들을 Mockito 등으로 Stubbing하지 않으므로 요청에 따른 DB 저장, 조회 등이 실제로 발생한다. 즉, 기존에 사용하던 RestAssured를 대체하여 사용할 수 있다.

RANDOM_PORT에서는 사용할 수 없던 @Transactional을 붙였으므로 테스트 환경에서 알아서 메서드별로 rollback이 발생한다. 따라서 application.properties를 통해 초기 데이터만 설정해주면 전체 테스트에서 일관된 DB 데이터를 활용할 수 있다.

 

간단히 컨텍스트만을 로드하는 테스트를 통해 확인해보았을 때 @SpringBootTest의 WebEnvironment 설정을 기본인 MOCK으로 하는 테스트와 RANDOM_PORT을 사용하는 테스트 간에는 5~10% 정도의 속도 차이가 있었다.

 

 


 

테스트는 이후의 리팩토링을 위해 매우 중요하지만 사용하는 도구와 검증하려는 범위에 따라 방식이 천차만별이다.

그때그때 적절한 방법을 찾아가도록 하자.

반응형