Spring

1. argumentsAreDifferent 에러 발생

Mockito는 Java 단위 테스트를 위한 라이브러리이다. 나는 Controller를 테스트할 때 실제 Service를 호출하지 않고 Mocking해서 테스트하기 위해 사용했다.

@AutoConfigureMockMvc(addFilters = false)
@WebMvcTest(value = MemberController.class)
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    MemberService memberService;

    @Test
    @DisplayName("클라이언트의 요청에 따라 신규 회원을 등록한다.")
    void register() throws Exception {
        // given
        MemberRegisterDto dto = ew MemberRegisterDto("email", "password");

        // when
        mockMvc.perform(MockMvcRequestBuilders.post("/member"))
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(dto))
            .andExpect(status().isOK());

        // then
        verify(memberService).register(dto);
    }
}
png

Mockito의 verify() 메서드는 특정 동작이 한 번 일어났는지 확인한다.

  1. MemberController의 '/member' 주소로 'POST' 요청

  2. MemberController의 memberService.register() 메서드 호출

당연히 한 번 호출했으니 제대로 비교가 될 것이라고 여겼는데 아래와 같은 에러가 발생했다.

Argument(s) are different! Wanted:
com.example.member.service.MemberService#0 bean.saveMember(
    com.example.member.dto.MemberRegisterDto@3847fe81
);
-> at com.example.member.controller.MemberControllerTest.createMember(MemberControllerTest.java:60)
Actual invocations have different arguments:
com.example.member.service.MemberService#0 bean.saveMember(
    com.example.member.dto.MemberRegisterDto@4b85a64
);
-> at com.example.member.controller.MemberController.createMember(MemberController.java:32)

요약하면 현재 MemberCotrollerTest의 MemberRegisterDto와 실제 MemberController의 MemberRegisterDto 객체가 다르다는 것이다. 객체의 equals()를 주소값으로 판단하기에 MemberRegisterDto가 controller -> service를 거치며 변하는 경우 비교하는 객체가 달라졌다고 인식하는 것이다.

2. refEq() 메서드로 해결

문제를 해결하려면 주소값이 아닌 실제 값을 비교하도록 equals() 메서드를 수정하거나 최초 객체를 비교해야 한다.

png

그리고 refEq() 메서드를 활용하라는 조언을 발견했다.

png

refEq() 메서드는 비교 객체에 대해 equals가 구현되지 않은 경우 사용할 수 있다. Matcher는 Java Reflection API를 사용해서 원하는 객체와 실제 객체의 필드를 비교한다.

@AutoConfigureMockMvc(addFilters = false)
@WebMvcTest(value = MemberController.class)
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    MemberService memberService;

    @Test
    @DisplayName("클라이언트의 요청에 따라 신규 회원을 등록한다.")
    void register() throws Exception {
        // given
        MemberRegisterDto dto = ew MemberRegisterDto("email", "password");

        // when
        mockMvc.perform(MockMvcRequestBuilders.post("/member"))
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(dto))
            .andExpect(status().isOK());

        // then
        verify(memberService).register(refEq(dto));
    }
}

다시 테스트를 실행시켜보았다.

png

테스트가 정상적으로 실행된다. 이제 문제가 잘 해결된 것일까?

3. 또 다시 argumentsAreDifferent 에러 발생

이번에는 다른 테스트에서 argumentsAreDifferent 에러가 발생했다.

class OrderControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    OrderService orderService;

    @Test
    @DisplayName("클라이언트의 요청에 따라 신규 주문을 등록한다.")
    void register() throws Exception {
        // given
        List<Items> items = List.of(new Item("item1", 100, 1), new Item("item2", 200, 2));
        OrderRequestDto dto = new OrderRequestDto("order", items);

        // when
        mockMvc.perform(MockMvcRequestBuilders.post("/order"))
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(dto))
            .andExpect(status().isOK());

        // then
        verify(orderService).createOrder(refEq(dto));
    }
}

이번에는 refEq() 메서드도 적용했는데 무엇이 문제일까?

Argument(s) are different! Wanted:
com.example.order.service.OrderService#0 bean.saveOrder(
    com.example.order.dto.OrderRequestDto@120e3ebb
);
-> at com.example.order.controller.OrderControllerTest.createOrder(OrderControllerTest.java:60)
Actual invocations have different arguments:
com.example.order.service.OrderService#0 bean.saveOrder(
    com.example.order.dto.OrderRequestDto@1a29c848
);
-> at com.example.order.controller.OrderController.createOrder(OrderController.java:32)

원인은 OrderRequestDto에 있었다.

@Getter
@AllArgsConstructor
public class OrderRequestDto {
	@NotBlank
	private String price;

	@NotNull
	private Long count;

	@NotNull
	private List<Items> items;
}

OrderRequestDto는 중첩된 객체라 refEq() 메서드로 비교할 수 없었던 것이다. items 필드를 null로 변경하니 refEq() 메서드가 제대로 동작했고 테스트를 통과할 수 있었다. 그러나 중첩 객체의 모든 필드를 null로 만들 수는 없으므로 다른 방안을 찾아보기로 했다.

4. DeepReflectionEqMatcher 구현

png

23년 10월 기준 Mockito는 deepRefEq()를 지원하지 않았다. 그러므로 중첩된 객체를 비교하려면 직접 ArgumentMatcher를 구현해야 한다.

png

위의 깃헙 이슈 아래 달린 구현 예시는 다음과 같다.

public class DeepReflectionEqMatcher<T> implements ArgumentMatcher<T> {
    private final T expected;
    private final String[] excludedFields;

    public DeepReflectionEqMatcher(T expected, String... excludeFields) {
        this.expected = expected;
        this.excludedFields = excludeFields;
    }

    @Override
    public boolean matches(Object argument) {
        try {
            assertThat(argument)
                    .usingRecursiveComparison()
                    .ignoringFields(excludedFields)
                    .isEqualTo(expected);
            return true;
        } catch (Throwable e) {
            return false;
        }
    }

    public static <T> T deepRefEq(T value, String... excludeFields) {
        return argThat(new DeepReflectionEqMatcher<T>(value, excludeFields));
    }
}

이제 다시 OrderControllerTest를 호출해보자.

class OrderControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    OrderService orderService;

    @Test
    @DisplayName("클라이언트의 요청에 따라 신규 주문을 등록한다.")
    void register() throws Exception {
        // given
        List<Items> items = List.of(new Item("item1", 100, 1), new Item("item2", 200, 2));
        OrderRequestDto dto = ew OrderRequestDto("order", sellerCreateDto);

        // when
        mockMvc.perform(MockMvcRequestBuilders.post("/order"))
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(dto))
            .andExpect(status().isOK());

        // then
        verify(orderService).createOrder(deepRefEq(dto));
    }
}

테스트가 정상적으로 통과하는 걸 확인할 수 있었다.

참고 자료

Last updated