프로그래밍 일기 — Integration Testing
디버깅은 프로그래밍의 숙명
프로그래밍 일기 — Integration Testing
디버깅은 프로그래밍의 숙명

버그를 없애고 프로그램을 원할하게 동작하게하는 것은 프로그래머의 사명이다(1).
프로그램은 버그라는 기생충을 안고 사는 숙명이 있다. 사람처럼 철저하게 관리하려해도, 왠만한 프로그램은 초기 개발시 버그가 꽤 있는 것이 현실이다. 프로그램은 버그로부터, 적어도 초기에는 100% 자유로워지기 힘들다.
그렇다면 버그를 잡을 수 있는 장치들을 고안해야할 것이다. 프로그램이 개발된 후 품질 인증 과정을 거치려면 반드시 이 프로그램이 주어진 상황에서 기댓값을 출력하는지 봐야한다. 그러한 목적으로 테스트 코드라는 것을 작성한다. 일반적으로 개발자들은 자신의 프로그램이 테스트 통과한다는 것을 입증하기 위해 테스트 코드를 작성한다. 그 테스트를 통과하면, 그때야 비로소 프로그램이 잘 동작한다고 말할 수 있다.
하지만 테스트 코드 자체도 잘 작성이 되어있는지 확인해야한다. 테스트 코드가 명확하지 않게 짜여져있으면 출력값을 제대로 확인하지 않을 수도 있고, 타임아웃되어 프로그램을 신뢰할 수 없게된다. 좋은 개발자가 되려면 그만큼 좋은 테스트 코드를 작성해야하는 것은 숙명이다.
이번 프로젝트에서도 테스트 코드를 작성해보았다. 먼저 ChatGPT-3에게 우리가 작성한 Service단에서의 코드를 주고 이 것을 테스트할 수 있는 코드를 작성 부탁했다. 그렇게하니 코드를 보면서 테스트를 위해 무엇을 활용해야하는지를 조금 더 잘 알 수 있었다(예: Mockito / Junit5). 비록 프로그램의 일부 컴포넌트에 대한 단위 테스트에 불과했지만 말이다.
그래서 통합테스트는 어떻게 작성해야할지에 대해 궁금증이 생겼다. 보통 이러한 형태의 테스트는 프로그램의 UI상에서 시나리오를 기반으로 수행된다(보통 QA라는 부서의 일이다.). 하지만, 나는 개발자로써 코드 상에서 무언가를 할 수 있는지 찾고 싶다. 운 좋게도 Spring에서 Integration Testing을 하는 방법에 대해 소개한 글이 있었다. 이번 글에서는 이 문서의 내용을 참조하여 통합테스트를 작성하는 방법에대해 기록해보려한다.
Spring Boot에서 Integration Testing(2)
1. 준비 사항
일단 JUnit를 사용할 준비가 필요하다. Jupiter-engine 과 Jupiter-api 가 필요하다. 추가적으로 springframework 에서 제공하는 test 도 필요하다. 추가적으로 결과값을 assert 테스트하기 위해 유용한 hamcrest 와 jsonpath 도 가져온다.
아래 예제에서는 @ContextConfiguration 에서 ApplicationConfig.class 설정 클래스가 제공되는데, 여기서는 특정 테스트에 필요한 설정을 불러온다.
// Build.gradle
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// JUnit5
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
// Jsonpath
implementation 'org.json:json:20211205'
// hamcrest
testImplementation 'org.hamcrest:hamcrest:2.2'
2. Spring MVC 테스트 설정
JUnit5는 JUnit Test와 함께 통합이 가능한 인터페이스를 명기하는 방식으로 작동한다. @Extendwith Annotation 을 테스트할 클래스에 추가하고 확장(extension)할 클래스를 명기한다. Spring 테스트를 실행하려면 주로 SpringExtension.class 를 활용한다.
추가적으로 @ContextConfiguration Annotation을 활용하여 컨텍스트 설정을 불러오고 테스트가 사용할 컨텍스트를 함께 묶어(bootstrap)해준다.
**@ContextConfiguration(locations={""}) : **Java 설정(Configuration) 클래스를 사용하여 컨텍스트를 설정 하듯이, XML 설정을 사용할 수 있다.
**@WebAppConfiguration(value = "") : **여기에 웹 어플리케이션 컨텍스트를 불러올 수 있는 @WebAppConfiguration Annotation을 활용한다. 기본적으로 경로 src/main/webapp 에서 루트 웹 어플리케이션을 찾도록 한다. 이 경로는 value 속성을 정의하여 overriding이 가능하다.
// 사용할 Spring Test 정의
@ExtendWith(SpringExtension.class)
// 컨텍스트 설정
@ContextConfiguration(classes = { ApplicationConfig.class })
@WebAppConfiguration
public class GreetControllerIntegrationTest {
....
}
WebApplicationContext 객체는 웹 어플리케이션 설정을 제공한다. 모든 어플리케이션의 bean 및 컨트롤러를 컨텍스트에 불러온다. 이 설정을 통해 웹 어플리케이션 컨텍스트를 테스트에 연결할 수 있는 준비를 할 수 있다.
@Autowired
private WebApplicationContext webApplicationContext;
3. 웹 컨텍스트 Bean을 Mocking하기
MockMvc 는 Spring MVC 테스트를 지원한다. 이 것은 모든 웹 어플리케이션에 대한 bean 을 포함(encapsulate)하며, 테스트에 활용 가능하게 끔 한다.
@BeforeEach Annotation 을 통해 mockMvc 객체를 초기화할 수 있다. 이렇게 전역(global)으로 선언하면, 모든 테스트마다 별도로 초기화 할 필요가 없다.
private MockMvc mockMvc;
@BeforeEach
public void setup() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
}
WebApplicationContext 객체가 제대로 불러왔는지, 또한 servletContext 가 잘 추가되었는지 확인할 필요가 있다. 또한 Spring Bean이 잘 불러와졌는지를 확인하기위해 GreetController.java 가 웹 컨텍스트에 불려와 졌는지를 확인할 수 있다. 이 시점에서 통합테스트를 위한 준비는 모두 끝난다. 이제 MockMvc 객체를 활용한 리소스 메서드를 테스트할 수 있다.
@Test
public void givenWac_whenServletContext_thenItProvidesGreetController() {
// ServletContext 인스턴스화
ServletContext servletContext = webApplicationContext.getServletContext();
// serveletContext가 잘 불러와졌는지 확인
Assert.assertNotNull(servletContext);
// servlet이 MockServletContext 타입인지 확인
Assert.assertTrue(servletContext instanceof MockServletContext);
// GreetController.java Bean이 웹 컨텍스트에 존재하는지 확인
Assert.assertNotNull(webApplicationContext.getBean("greetController"));
}
4. 통합테스트 작성
테스트 프레임워크를 통해 활용할 수 있는 기본적인 동작에 대해 알아보자. 예를 들어 Path Variable 방식으로 요청을 매개변수와 함께 보내는 방식의 동작을 상상할 수 있다. 또한 적합한 View의 이름이나 Response Body상에서의 내용을 어떻게 assert 할 수 있는지에 대한 예제를 확인해보자.
View 이름 확인 테스트
아래의 코드 스니펫은M*ockMvcRequestBuilders 및 `MockMvcResultMatchers*에서static`import 한 예제를 보여준다. 먼저 테스트 하려는 웹 어플리케이션의 엔드포인트를 확고히 할 필요가 있다.
/homePage엔드포인트 명기 방식 1:[http://localhost:8080/spring-mvc-test/](http://localhost:8080/spring-mvc-test/)/homePage엔드포인트 명기 방식 2:[http://localhost:8080/spring-mvc-test/homePage](http://localhost:8080/spring-mvc-test/homePage)perform()메서드를 통해 GET 요청 메서드를 호출할 수 있다. 이는ResultActions라는 객체를 리턴하는데, 이 결과물로 response에 대한 기대값(HTTP status, header, 혹은 기타 응답 컨텐츠)을 assert할 수 있다.andDo(print())메서드로 요청과 응답을 출력한다. 에러가 발생할 경우 입출력을 상세하게 확인할 때 유용하다.andExpect()는 제공된 인자(argument)를 기대하는데, 예를 들어 아래 예제에서는MockMvcResultMatchers.view()를 통해index가 리턴되는 것을 기대한다.
@Test
public void givenHomePageURI_whenMockMVC_thenReturnsIndexJSPViewName() {
this.mockMvc.perform(get("/homePage")).andDo(print())
.andExpect(view().name("index"));
}
Response Body 확인 테스트
/greet 엔드포인트를 활용해 response body를 확인할 수 있다([http://localhost:8080/spring-mvc-test/greet](http://localhost:8080/spring-mvc-test/greet) ).
기대값은 아래와 같다.
{
"id": 1,
"message": "Hello World!!!"
}
다음으로 테스트 코드를 작성해볼 수 있다. 아래 예제는 다음과 같은 로직을 담고 있다.
andExpect(MockMvcResultMatchers.status().isOk()): 는 response의 HTTP status가 OK(200)인지 확인한다. 요청이 성공적으로 수행되었는지를 확인하는 것이다.andExpect(MockMvcResultMatchers.jsonPath(“$.message”).value(“Hello World!!!”)): response 내용이"Hello World!"와 매칭되는지를 확인한다.jsonPath를 활용해 response 내용을 추출하고 해당 필드의 확인된 값을 제공한다.andReturn()는MvcResult객체를 반환한다. 이는 라이브러리를 통해 직접적으로 확인이 불가능한 것들을 체크하기 위해 활용되는데, 여기서는assertEquals를 통해MvcResult객체에서 추출한 response의 타입과 매칭되는지를 확인한다.
// 테스트 코드 예제
@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() {
MvcResult mvcResult = this.mockMvc.perform(get("/greet"))
.andDo(print()).andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Hello World!!!"))
.andReturn();
Assert.assertEquals("application/json;charset=UTF-8",
mvcResult.getResponse().getContentType());
}
Path Variable 방식 테스트
다음으로 Path Variable방식으로 GET 요청을 해 볼 수 있다. /greetWithPathVariable/{name} 엔드포인트를 활용하여 값을 출력할 수 있는데, 우리가 기대하는 값은 아래와 같다.
// 기대 출력값
{
"id": 1,
"message": "Hello World John!!!"
}
위 정보를 기반으로 아래와 같은 테스트 코드를 작성해볼 수 있다. 설명하자면 MockMvcRequestBuilders.get(“/greetWithPathVariable/{name}”, “John”) 는 /greetWithPathVariable/John 라는 형태로 요청을 한다. 이렇게하면 동적으로 URL내 매개변수를 설정할 수 있고, 가독성도 좋아진다. Path Parameter를 이러한 방식으로 얼마든지 보낼 수 있다.
@Test
public void givenGreetURIWithPathVariable_whenMockMVC_thenResponseOK() {
this.mockMvc
.perform(get("/greetWithPathVariable/{name}", "John"))
.andDo(print()).andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World John!!!"));
}
Query Parameter 방식 테스트
/greetWithQueryVariable?name={name} 를 이용해 테스트를 할 수도 있다: [http://localhost:8080/spring-mvc-test/greetWithQueryVariable?name=John%20Doe](http://localhost:8080/spring-mvc-test/greetWithQueryVariable?name=John%20Doe) 와 같은 방식을 활용하는 것이다.
기대 값은 아래와 같다.
{
"id": 1,
"message": "Hello World John Doe!!!"
}
GET 요청에 Query할 매개변수를 붙여 요청할 수 있다(param(“name”, “John Doe”)) 이전에 언급한 /greetWithQueryVariable?name=John%20Doe 와 비슷하고, URI 양식을 통해 쿼리할 변수를 적용할 수 있다.
this.mockMvc.perform(
get("/greetWithQueryVariable?name={name}", "John Doe"));
POST 요청 테스트
/greetWithPost 엔드포인트를 활용해 테스트를 할 수 있다: [http://localhost:8080/spring-mvc-test/greetWithPost](http://localhost:8080/spring-mvc-test/greetWithPost)
기댓값은 아래와 같다.
{
"id": 1,
"message": "Hello World!!!"
}
MockMvcRequestBuilders.post(“/greetWithPost”) 를 통해 POST 요청을 할 수 있다. Path Variable과 Query Parameter를 이전과 비슷한 방식으로 설정할 수 있다. form 데이터를 쓴다면 오직 param() 메서드에서만 가능할 것이다.
@Test
public void givenGreetURIWithPost_whenMockMVC_thenVerifyResponse() {
this.mockMvc.perform(post("/greetWithPost")).andDo(print())
.andExpect(status().isOk()).andExpect(content()
.contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World!!!"));
}
Query Parameter에 대한 테스트는 아래와 같다. 먼저 요청은 [http://localhost:8080/spring-mvc-test/greetWithPostAndFormData](http://localhost:8080/spring-mvc-test/greetWithPostAndFormData) 를 통해 이루어지고 POST 요청과 함께 입력되는 변수는 id=1;name=John%20Doe 이며, 기댓값은 아래와 같다.
{
“id”: 1,
“message”: “Hello World John Doe!!!”
}
위 조건하에 Query Parameter는 아래와 같이 POST 요청을 테스트할 수 있다. id 필드와 값인 "John Doe" 를 각각 1 과 "John Doe” 로 붙여졌다.
@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/greet"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!"))
.andReturn();
assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}
MovkMvc 제한사항
MockMvc 는 사용하기 쉬운 API를 통해 웹 엔드포인트를 호출하고 동시에 response의 내용을 검증하고 assert할 수 있게 해준다. 그러나 이러한 장점에도 불구하고, 몇 가지 한계가 존재한다.
DispatcherServlet 의 subclass를 활용하여 테스트 요청을 처리한다. 예 를들어 TestDispatcherServlet 은 컨트롤러를 호출하고 Spring의 기능을 모사(mock)할 책임이 있다. MockMvc 클래스는 내부에서 이 TestDispatcherServlet 을 wrapping한다. 매번 perform() 메서드를 사용하여 요청을 보낼 때, MockMvc 객체는 이러한 TestDispatcherServlet 를 직접 활용한다. 따라서 실제로 네트워크로 연결이 되는 것은 아니며, 결과적으로 MockMvc 를 통해 전체 네트워크 스택을 테스트할 수 없는 문제가 있다.
Spring이 가짜 웹 어플리케이션 컨텍스트를 생성하여 HTTP 요청과 응답을 모사(mock)하기 때문에, 스프링 어플리케이션의 모든 기능을 지원하지 못할 수 있다. 예를 들어 위와같은 mock 설정은 HTTP redirection을 지원하지 않는다. Spring Boot가 만약 현재 요청을 /error 엔드포인트에 redirect할 때 에러가 생긴다면 이를 테스트할 수 없다는 의미다. 따라서 MockMvc 객체만을 사용하여 모든 API의 에러 케이스를 확인할 수 없을 수도 있다. 이러한 문제를 보완하기ㅟ해 더 실제와 유사한 어플리케이션 컨텍스트를 설정하고RestTemplate 혹은 REST-assured 를 활용해 어플리케이션을 테스트 하기도한다. 예를 들어 아래와 같다.
아래 코드에서는 @ExtendWith(SpringExtension.class). 를 쓰지않고 테스트 코드를 작성했다. 이 방식으로 모든 테스트는 실제 HTTP 요청을, 무작위적으로 선택된 TCP 포트에 listen하는 어플리케이션에 대해 할 수 있다. PORT 번호를 직접 지정하여 확실하게 믿을 수 있는 HTTP 요청 및 응답을 할 수 있다.
@SpringBootTest(webEnvironment = DEFINED_PORT)
public class GreetControllerRealIntegrationTest {
@Before
public void setUp() {
RestAssured.port = DEFAULT_PORT;
}
@Test
public void givenGreetURI_whenSendingReq_thenVerifyResponse() {
given().get("/greet")
.then()
.statusCode(200);
}
}
요약
WebApplicationContext및MockMvc객체는 어플리케이션의 엔드포인트를 호출하는데 있어 매우 유용한 기능을 제공한다.Path Variable, Query Parameter, Request Body등으로 HTTP 요청을 할 수 있고, HTTP의 status, header, 및 내용을 테스트할 수 있다.MockMvc객체의 한계가 존재한다: 실제 HTTP 요청이 이루어졌는지 믿기 어렵고, 모든 redirection 등을 포함한 모든 어플리케이션의 다른 기능을 테스트하기 어렵다. 이에 대한 대안으로RestTemplate및REST-assured를 활용가능하다.
위 모든 코드 내용은 참조(3)에서 확인가능하다.
참조:
(1) https://pixabay.com/photos/code-code-debug-learn-sleep-repeat-4918187/
(2) https://www.baeldung.com/integration-testing-in-spring
(3) https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-java
메타데이터
- post_id
- 3879fa2a1ab8
- slug
- 프로그래밍-일기-integration-testing-3879fa2a1ab8
- url
- https://medium.com/@conanmoon/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9D%BC%EA%B8%B0-integration-testing-3879fa2a1ab8
- canonical_url
- https://medium.com/@conanmoon/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9D%BC%EA%B8%B0-integration-testing-3879fa2a1ab8
- author_url
- https://medium.com/@conanmoon
- status
- ok
- fetched_at
- 2026-07-09 13:13:48