프로젝트 진행 중 Toss payments API를 활용하여 카드 결제 시스템을 구현 중이었다. 토스 외부 API 서버를 사용하는 통합테스트가 아닌 단위 테스트를 하려고 했다.
그 이유는 다음과 같다.
- 독립성: 외부 서버를 Mocking하면 테스트 시에 외부 서버의 상태나 가용성에 영향을 받지 않는다. 이는 테스트의 안정성과 신뢰성을 높여준다.
- 빠른 테스트 속도: 외부 서버를 호출하는 테스트는 네트워크 지연으로 인해 느려질 수 있다. 하지만 Mocking을 통해 실제 네트워크 호출을 회피하면 테스트가 더 빠르게 실행된다.
- 테스트 제어: Mocking을 통해 응답을 조작할 수 있으므로 다양한 시나리오를 시뮬레이션하여 테스트를 보다 철저하게 수행할 수 있다. 예를 들어, 다양한 오류 조건이나 응답 상태를 강제로 시뮬레이션하여 애플리케이션의 안정성과 견고성을 테스트할 수 있다.
- 테스트 환경의 안정성: 외부 서버가 계속해서 변경되는 경우, 테스트 환경이 불안정해질 수 있다. 하지만 Mocking을 사용하면 외부 서버의 변경에 영향을 받지 않으므로 테스트의 일관성을 유지할 수 있다.
Mocking
을 하면 위에 도메인 모델을 아래와 같이 바꿀 수 있다.
@RunWith(SpringRunner.class)
@WebMvcTest(PaymentController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureMockMvc(addFilters = false)
class PaymentControllerTest extends CreatePaymentData {
@Autowired
private MockMvc mockMvc;
@MockBean
private TossAPIService tossAPIService;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testKeyIn() throws Exception {
CardPaymentDto.Response mockResponse = new CardPaymentDto.Response(cardPayment, "카드");
// Serialize the mock response to JSON
String mockResponseJson = objectMapper.writeValueAsString(mockResponse);
given(tossAPIService.callExternalAPI(any(CardPaymentDto.Request.class))).willReturn(mockResponseJson);
CardPaymentDto.Request request = new CardPaymentDto.Request(cardPayment);
mockMvc.perform(post("/pay/v1/payments/card")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orderName").value("커피 외 3개"))
.andExpect(jsonPath("$.orderId", endsWith("00000")))
.andExpect(jsonPath("$.totalAmount").value("3000.0"))
.andExpect(jsonPath("$.method").value("카드"))
.andExpect(jsonPath("$.cardNumber", containsString("12341234")));
}
}
테스트 코드에서 사용된 mockMvc는 Spring MVC의 Mock 객체로 컨트롤러를 테스트하기 위한 가상의 요청과 응답을 처리하는 데 사용된다.tossAPIService
는 Mock 객체
로 대체되어 있으며, given(tossAPIService.callExternalAPI(any(CardPaymentDto.Request.class))).willReturn(mockResponseJson)
구문을 통해 callExternalAPI 메서드가 호출될 때 가짜 응답 데이터를 반환하도록 설정한다. 이렇게 함으로써 외부 API 호출과 관련된 로직을 분리하고 테스트할 수 있다.
(@WebMvcTest
를 통해 지정한 Bean이 갖는 의존성은 @MockBean
으로 등록해야 한다. -> 외부 API서버를 호출하는 tossAPIService는 MockBean으로 지정)
@WebMvcTest
는 JPA 생성과 관련된 기능이 전혀 존재하지 않는 테스트 어노테이션이다.
JPA Auditing 기능을 사용할때 @SpringBootApplication
에 @EnableJPaAuditing
을 추가하여 사용하고 있기 때문에 발생하는 에러이다.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
} // 따로 @Configration으로 Bean 설정해주면 테스트 클래스 일일이 어노테이션 붙일 필요 없음
MockHttpServletResponse:
Status = 403
Error message = null
Headers = [Content-Type:"text/plain"]
Content type = text/plain
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
컨트롤러는 모델 단의 테스트와 다르게 서블릿과 연관된 부분이 많아 컨텍스트를 로드하고 테스트한다.
컨트롤러 테스트는 컨텍스트를 올리고 진행하기 때문에 security, filter, config 등의 필터문제로 @AutoConfigureMockMvc 어노테이션을 통해 filter, 웹 드라이버 등을 지정해 줄 수 있기에 @AutoConfigureMockMvc(addFilters = false)
로 처리하면 CORS FIlter에서 Origin을 *
로 처리하고 호출하면 정상적 작동한다. 하지만 cors는 와일드카드(*)로 모든 출처 허용해주게 된다면 보안성이 낮아지므로 출처를 명시해주는게 좋다고 생각한다.
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("http://localhost:8080","http://localhost:3000");
}
mockMvc.perform(post("/pay/v1/payments/card")
.contentType(MediaType.APPLICATION_JSON)
...; // (중략)
초록색 ✅
MockWebServer
는 주로 HttpClient보다는 WebClient와 같은 라이브러리를 사용하여 웹 클라이언트를 구현할 때에 많이 사용된다.
MockWebServer은 실제로는 외부 API 호출을 시뮬레이트하는 가짜 웹 서버이기 때문에, 웹 클라이언트 라이브러리가 MockWebServer와 통신하면서 가짜 응답을 받을 수 있다. 따라서 웹 클라이언트 라이브러리의 동작을 테스트하고 싶을 때에는 MockWebServer를 사용하는 것이 가장 효율적이다.
하지만 HttpClient를 사용하는 경우에는 직접적인 외부 API 호출이 이루어지기 때문에, MockWebServer를 사용하지 않고도 Mock 객체를 활용하여 테스트할 수 있다. Mocking을 사용하면 외부 의존성을 제거하고 원하는 방식으로 응답을 가짜 데이터로 설정할 수 있으므로, 테스트를 더 쉽게 구현할 수 있다.
정보 감사합니다.