본문 바로가기
Study/spring

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

by 유경호 2021. 1. 14.
반응형

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

반응형