MockMvc와 random port

햄햄·2023년 3월 8일
0

테스트 메서드에 @Transactional 좋다 vs 안좋다

업무와 관련하여 테스트 코드에 대해 이야기를 나누던 중, 동료 분이 테스트 메서드에 @Transactional 어노테이션을 붙이는건 좋지 않을 수 있다며 블로그 링크를 하나 보내주셨다. 요약하자면, @Transactional 어노테이션이 붙은 테스트는 실패해야함에도 거짓된 성공을 할 수 있고, 데이터가 DB에 저장되지 않기 때문에 DB 문제를 알아내기 어렵다는 등의 이유로 좋지 않다는 내용이었다.

마침 이와 관련해서 토비님이 인프런에서 답변하신 글을 최근에 보았기에 다른 의견을 드렸다. 토비님은 테스트 메서드에 @Transactional을 쓰기를 적극 권장하신다고 한다. 이에 대해 꽤 길고 상세하게 적어주셨는데, 내가 요약하기엔 너무 어려운 내용이라 한번 직접 읽어보시길 바란다.

슬랙에서 우리의 대화를 보시던 팀장님도 의견을 곁들어 주셨다. 그런데... 스레드가 길어질수록 점점 정신이 혼미해졌다.

무슨 소리시죠...?

모르는 소리에 모르는 소리가 쌓이고 또 모르는 소리가 덧붙여지고... 토비님의 인프런 글부터 시작해서 슬랙 대화까지 내 인지능력은 서서히 늪으로 빨려 들어갔다... 이런 점이 어렵다. 모르는게 너무 많아 도대체 뭐부터 알아야할지 막막해진다는 점이다. 막막할 땐 제일 만만한 것부터 공략해야 한다. 슬랙 대화에서 MockMvc와 random port에 대한 언급이 나왔다. 그나마 쉬워보이는 MockMvc와 random port에 대해 알아보자.

MockMvc란

MockMvc는 Spring MVC 테스트를 지원하는 스프링의 클래스이다. 모든 웹 애플리케이션 빈을 캡슐화하여 테스트에 사용할 수 있도록 한다. 다음과 같이 사용할 수 있다.

private MockMvc mockMvc;
@BeforeEach
public void setup() throws Exception {
    this.mockMvc = MockMvcBuilders
    				.webAppContextSetup(this.webApplicationContext)
          		    .build();
}

@Test
public void givenHomePageURI_whenMockMVC_thenReturnsIndexJSPViewName() {
    this.mockMvc.perform(get("/homePage"))
    	.andDo(print())
        .andExpect(view().name("index"));
}

perform() 메서드는 GET 요청 메서드를 호출하고 ResultActions를 반환한다. 이를 사용하여 응답(response), 응답의 내용, HTTP status, header에 대한 기대값을 테스트할 수 있다. andDo(print())는 요청과 응답을 출력할 것이다. 에러가 발생할 경우 자세한 내용을 확인하는 데 유용하다. andExpect()는 전달된 인자를 예상(expect)한다. 이 경우에는 MockMvcResultMatchers.view()를 통해 "index"가 반환될 것으로 예상하고 있다.

MockMvc에는 몇가지 제한 사항이 있다. MockMvc 클래스는 내부적으로 DispatcherServlet의 서브 클래스인 TestDispatcherServlet을 래핑한다. perform() 메서드로 요청을 보낼 때마다 MockMvc는 TestDispatcherServlet을 사용한다. 따라서 실제 네트워크 연결이 이루어지지 않으므로 MockMvc를 사용하는 동안 전체 네트워크 스택을 테스트할 수 없다.

또한 Spring은 HTTP 요청과 응답을 모킹하기 위해 웹 애플리케이션 컨텍스트를 가짜로 준비하기 때문에 스프링 애플리케이션의 모든 기능을 지원하지 않을 수도 있다.

예를 들면, 이 모킹 설정은 HTTP 리다이렉션을 지원하지 않는다. 스프링 부트는 요청을 /error 엔드포인트로 리다이렉션하여 일부 오류를 처리하기 때문에, MockMvc를 사용하는 경우 일부 api 오류를 테스트하지 못할 수 있다.

MockMvc의 대안으로, 보다 실제적인 애플리케이션 컨텍스트를 설정한 다음 RestTemplate 또는 REST-assured를 사용하여 애플리케이션을 테스트할 수 있다.

출처: https://www.baeldung.com/integration-testing-in-spring

스프링 부트와 함께 테스트하기

@SpringBootTest
public class TestingWebApplicationTests {

	@Test
	public void contextLoads() {
	}

}

@SpringBootTest 어노테이션은 스프링 부트가 기본 구성 클래스(예: @SpringBootApplication이 있는 클래스)를 찾고 이를 사용하여 스프링 애플리케이션 컨텍스트를 시작하도록 지시한다. 다음과 같이 assertion을 추가할 수 있다.

@SpringBootTest
public class SmokeTest {

	@Autowired
	private HomeController controller;

	@Test
	public void contextLoads() throws Exception {
		assertThat(controller).isNotNull();
	}
}

스프링은 @Autowired 어노테이션을 해석하고, 컨트롤러는 테스트 메서드가 실행되기 전에 주입된다.

스프링 테스트에서는 테스트 간 애플리케이션 컨텍스트가 캐시된다. 하나의 테스트 케이스에 여러 메서드가 있거나 동일한 구성을 가진 여러 테스트 케이스가 있는 경우 애플리케이션을 시작할 때 한 번만 비용이 발생한다. @DirectContext 어노테이션을 사용하여 캐시를 제어할 수 있다.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {

	@Value(value="${local.server.port}")
	private int port;

	@Autowired
	private TestRestTemplate restTemplate;

	@Test
	public void greetingShouldReturnDefaultMessage() throws Exception {
		assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/",
				String.class)).contains("Hello, World");
	}
}

webEnvironment=RANDOM_PORT로 임의의 포트로 서버를 시작하여 테스트 환경에서 충돌을 피할 수 있다. @LocalServerPort로 포트를 주입하는 것에 유의해야 한다. 또한 Spring Boot에서 자동으로 TestRestTemplate을 제공한다. @Autowired를 추가하기만 하면 된다.

스프링이 HTTP 요청을 처리하고 이를 컨트롤러에 전달하도록 할 때 서버를 전혀 시작하지 않고 그 아래 레이어만 테스트하는 방식으로 접근해볼 수 있다. 이렇게 하면 서버를 시작하는 데 드는 비용 없이 실제 HTTP 요청을 처리하는 것과 똑같은 방식으로 코드가 호출된다. 이렇게 하려면 MockMvc를 사용하고 테스트 케이스에 @AutoConfigureMockMvc 어노테이션을 사용하여 주입되도록 요청하면 된다.

@SpringBootTest
@AutoConfigureMockMvc
public class TestingWebApplicationTest {

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void shouldReturnDefaultMessage() throws Exception {
		this.mockMvc
        	.perform(get("/"))
            .andDo(print())
            .andExpect(status().isOk())
			.andExpect(content().string(containsString("Hello, World")));
	}
}

출처: https://spring.io/guides/gs/testing-web/

여전히 어렵지만

여전히 위의 내용을 100% 이해하진 못했다. 그래도 안개가 한겹은 걷힌 기분이 든다. 아 그때 작성한 코드가 그래서.. 아 그때 하셨던 말씀이 그래서.. 라는 생각도 들었다. 알아야할게 아직 산더미지만 그래도 쪼끔이나마 나아갔다는 점에서 아주 뿌듯하다!

profile
@Ktown4u 개발자

0개의 댓글