Study/spring

MockMvc를 이용한 REST API의 Json Response 검증

유경호 2021. 1. 14. 21:00
반응형

MockMvc를 이용하여 API의 Json Response를 JsonPath 표현식을 사용해 검증하는 예제를 정리하였습니다.


테스트 예제를 보기 앞서 테스트에 필요한 기본적인 지식들을 간단히 서술 해보겠습니다.

 

의존성 추가

Spring Boot에 spring-boot-starter-test 의존성을 추가하여 테스트를 작성할 수 있습니다.

 

@WebMvcTest와 @MockBean을 이용한 테스트

  • @SpringBootTest 어노테이션을 사용하면 스프링이 관리하는 모든 빈을 등록시켜서 통합 테스트를 진행하기 때문에 무겁습니다.
  • 하지만 @WebMvcTest는 web 레이어 관련 빈들만 등록하므로 비교적 가볍습니다.
  • web레이어 관련 빈들만 등록되므로 Service는 등록되지 않습니다. 따라서 가짜로 만들어줄 필요가 있습니다. → @MockBean 활용

@MockBean

직접 Service단까지 가서 DB에 값을 얻어오는게 아닌 Service를 Mocking하여 서비스까지 가지 않고 개발자가 직접 임의의 값을 리턴하여 테스트할 수 있습니다.

@MockBean이란 어노테이션을 사용하면 기존에 있는 Bean을 MockBean으로 대체하여 개발자가 테스트할 때 값을 커스터마이징하기 쉬워집니다.


이제 테스트에 사용되는 Member 엔티티, MemberController, Response 시 사용될 DTO를 보겠습니다.

회원 Entity

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    @Embedded
    private Address address;
    
    @Embeddable
    @Getter
    public class Address {
	private String city;
	private String street;
	private String zipcode;

	public Address() {
	}
    
	public Address(String city, String street, String zipcode) {
		this.city = city;
		this.street = street;
		this.zipcode = zipcode;
	}
    }
}

회원 DTO

@Data 
@Builder
public class MemberDTO {

    private Long id;

    private String username;

    private Address address;
    
    @Getter
    public class Address {
	private String city;
	private String street;
	private String zipcode;

	public Address() {
	}
    
	public Address(String city, String street, String zipcode) {
		this.city = city;
		this.street = street;
		this.zipcode = zipcode;
	}
    }
}

회원 Controller

@RestController
public class MemberApiController {

    @GetMapping("/api/test/members")
    public List<MemberDTO> get() {
        List<Member> findMembers = memberService.findMembers();
        return findMembers.stream()
                        .map(
                            e -> MemberDTO.builder()
                            .id(e.getId())
                            .username(e.getUsername())
                            .address(e.getAddress())
                            .build())
                        .collect(Collectors.toList());
    }
}

초기화 메소드 initEach() 작성

@WebMvcTest(controllers = MemberApiController.class)
public class MemberApiTest {

    MockMvc mockMvc;

    @MockBean
    MemberService memberService;

    @BeforeEach
    void initEach() {
        this.mockMvc = standaloneSetup(new MemberApiController())
                .addFilter(new CharacterEncodingFilter("UTF-8", true))
                .defaultRequest(get("/api/test/members").accept(MediaType.APPLICATION_JSON).characterEncoding("UTF-8"))
                .alwaysExpect(status().isOk())
                .alwaysExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .alwaysDo(print())
                .build();

        List<Member> memberList = new ArrayList<>();
        Member member1 = new Member();
        member1.setId(1L);
        member1.setUsername("Kyeongho Yoo");
        member1.setAddress(new Address("서울", "오솔길", "123-123"));

        Member member2 = new Member();
        member1.setId(2L);
        member2.setUsername("Minho Yoo");
        member2.setAddress(new Address("제주", "올레길", "123-123"));

        Member member3 = new Member();
        member1.setId(3L);
        member3.setUsername("Cheolsu Kim");
        member3.setAddress(new Address("부산", "가락길", "123-123"));

        memberList.add(member1);
        memberList.add(member2);
        memberList.add(member3);

        when(memberService.findMembers()).thenReturn(memberList);
    }
}
  • initEach() 메소드는 @BeforeEach을 사용해 각 테스트 메소드 실행 직전에 초기화 작업을 진행
  • 각 테스트 당 mockMvc를 생성해 주입
  • mockService에 findMembers() 호출 시 반환 값 설정

아래는 위와 같이 환경을 구성했을 때 {get} /api/test/members 호출 시 반환되는 Json Response입니다.

[
    {
        "id": 1,
        "username": "Kyeongho Yoo",
        "address": {
            "city": "서울",
            "street": "오솔길",
            "zipcode": "123-123"
        }
    },
    {
        "id": 2,
        "username": "Minho Yoo",
        "address": {
            "city": "제주",
            "street": "올레길",
            "zipcode": "123-321"
        }
    },
    {
        "id": 3,
        "username": "Cheolsu Kim",
        "address": {
            "city": "부산",
            "street": "가락길",
            "zipcode": "123-123"
        }
    }
]

이제 jsonPath를 이용한 검증 예제와 Hamcrest가 제공하는 라이브러리로 작성한 간단한 예제들을 살펴 보겠습니다.

JsonPath 표현식 문법은 https://github.com/json-path/JsonPath를 참고해주세요.

 

테스트 메소드 예제 목록

  • JsonPath에 해당하는 값이 존재하는지
  • JsonPath에 해당하는 값이 존해하지 않는지
  • JsonPath에 해당하는 값의 동일성 비교
  • JsonPath로 가져온 값을 hamcrestMatcher로 검증하기

 

JsonPath에 해당하는 값이 존재하는지

    @Test
    public void exists() throws Exception {
        String expectByUsername = "$.[?(@.username == '%s')]";
        String addressByCity = "$..address[?(@.city == '%s')]";

        this.mockMvc.perform(get("/api/test/members"))
                .andExpect(jsonPath(expectByUsername, "Kyeongho Yoo").exists())
                .andExpect(jsonPath(expectByUsername, "Minho Yoo").exists())
                .andExpect(jsonPath(expectByUsername, "Cheolsu Kim").exists())
                .andExpect(jsonPath(addressByCity, "서울").exists())
                .andExpect(jsonPath(addressByCity, "제주").exists())
                .andExpect(jsonPath(addressByCity, "부산").exists())
                .andExpect(jsonPath("$..['username']").exists())
                .andExpect(jsonPath("$[0]").exists())
                .andExpect(jsonPath("$[1]").exists())
                .andExpect(jsonPath("$[2]").exists());
    }
  • "$.[?(@.username == '%s')]" Json 표현식에 %s에 파라미터를 바인딩하여 검색할 수 있음
  • 검색한 값이 존재하는 지 검증 할 수 있음
  • "$..address[?(@.city == '%s')]"와 같이 JsonPath 표현식을 잘 조합하면 자식까지 검색 가능
  • "$..['username']" 처럼 해당 요소 자체가 존재하는 지 검증 가능
  • "$[0]" 처럼 몇 번째 요소가 존재 하는 지 검증 가능

 

JsonPath에 해당하는 값이 존해하지 않는지

    @Test
    public void doesNotExist() throws Exception {
        this.mockMvc.perform(get("/api/test/members"))
            .andExpect(jsonPath("$.[?(@.username == 'Kyeongho Yoooooooo')]").doesNotExist())
            .andExpect(jsonPath("$.[?(@.username == 'Cheolsu Kiiiiiiim')]").doesNotExist())
            .andExpect(jsonPath("$[3]").doesNotExist());
    }
  • doesNotExist()를 사용하면 JsonPath로 값을 색적하여 값이 존재하지 않는지 검증할 수 있음

 

JsonPath에 해당하는 값의 동일성 비교

    @Test
    public void equality() throws Exception {
        this.mockMvc.perform(get("/api/test/members"))
            .andExpect(jsonPath("$[0].username").value("Kyeongho Yoo"))
            .andExpect(jsonPath("$[1].username").value("Minho Yoo"));

        // Hamcrest matchers...
        this.mockMvc.perform(get("/api/test/members"))
            .andExpect(jsonPath("$[0].username").value(equalTo("Kyeongho Yoo")))
            .andExpect(jsonPath("$[0].address.city").value(equalTo("서울")));
    }
  • JsonPath로 특정한 값을 동일성 비교하여 검증할 수 있음

 

JsonPath로 가져온 값을 hamcrestMatcher로 검증하기

    @Test
    public void hamcrestMatcher() throws Exception {
        this.mockMvc.perform(get("/api/test/members"))
            .andExpect(jsonPath("$[0].username", startsWith("Kyeong")))
            .andExpect(jsonPath("$[0].username", endsWith("Yoo")))
            .andExpect(jsonPath("$[0].username", containsString("ho Yo")))
            .andExpect(jsonPath("$[0].username", is(in(Arrays.asList("Kyeongho Yoo", "Minho Yoo")))));
    }
  • startsWith(), endsWith(), containsString(), is(in()) 등 Hamcrest가 제공하는 검증 메소드를 사용하여 다양한 방법으로 값을 검증할 수 있다.

 

    @Test
    public void hamcrestMatcherWithParameterizedJsonPath() throws Exception {
        String expectByUsername = "$[%s].username";
        String addressByCity = "$[%s].address.city";

        this.mockMvc.perform(get("/api/test/members"))
            .andExpect(jsonPath(expectByUsername , 0).value(startsWith("Kyeong")))
            .andExpect(jsonPath(expectByUsername , 2).value(endsWith("Kim")))
            .andExpect(jsonPath(expectByUsername , 1).value(containsString("nho Yo")))
            .andExpect(jsonPath(addressByCity , 1).value(is(in(Arrays.asList("서울", "제주", "부산")))));
    }
  • 위와 같이 Hamcrest 검증 메소드를 jsonPath().value()에 사용할 수도 있다.

Reference

반응형