12 분 소요

스프링 복습 및 정리 14.3P

슬라이스 테스트란?

단위 테스트의 경우 일반적으로 모듈이나 계층, 기술에 의존적이지 않도록 작성하는 것이 좋다.

그런데 단위 테스트 만으로는 애플리케이션의 모든 기능이 정상적으로 동작한다라고 보장되지는 않는다.

하나의 애플리케이션은 계층별로 역할이 있고,

계층별로 서로 연동되기 때문에 각각의 계층 별로 잘 동작하는지 테스트를 진행한 후에

마지막으로 통합테스트를 통해서 계층 간의 연동에 문제가 없는지 확인해야

비로소 개발자의 테스트 작업이 마무리되는 것이라고 할 수 있다.

이처럼 개발자가 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서 테스트하는 것을 슬라이스 테스트(Slice Test)라고 한다.

개발자가 통합 테스트까지 작성하면 좋겠지만 일정 상의 이유 등으로 인해

통합 테스트는 QA 부서에서 진행하는 기능 테스트로 대체되는 경우가 많다.

그리고 통합 테스트는 아니지만 QA 부서에서 본격적으로 전체적인 기능 테스트를 진행하기 전에

애플리케이션의 특정 수정 사항으로 인해 영향을 받을 수 있는 범위에 한해서 제한된 테스트를 진행하기도 한다.

테스트 세계에서는 이를 스모크 테스트(Smoke Test)라고 부른다.

API 계층 테스트

API 계층의 테스트 대상은 대부분 클라이언트의 요청을 받아들이는 핸들러인 Controller이다.

Spring에서는 Controller를 테스트하기 위한 편리한 방법들을 제공한다.

테스트 클래스 구조

@SpringBootTest       // (1)
@AutoConfigureMockMvc // (2)
public class ControllerTestDefaultStructure {
    // (3)
    @Autowired
    private MockMvc mockMvc;
    
    // (4) 
    @Test
    public void postMemberTest() {
        // given (5) 테스트용 request body 생성
        
        // when (6) MockMvc 객체로 테스트 대상 Controller 호출
        
        // then (7) Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증 
    }
}

1

(1)의 @SpringBootTest 어노테이션은 Spring Boot 기반의 애플리케이션을 테스트하기 위한 Application Context를 생성한다.

Application Context에는 애플리케이션에 필요한 Bean 객체들이 등록되어 있다.


2

(2)의 @AutoConfigureMockMvc 어노테이션은 Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해준다.

개발자들이 Spring Boot의 자동 구성을 통해 애플리케이션의 설정을 손쉽게 사용하듯이

@AutoConfigureMockMvc 어노테이션을 추가함으로써 테스트에 필요한 애플리케이션의 구성이 자동으로 진행된다.

(3)의 MockMvc 같은 기능을 사용하기 위해서는 @AutoConfigureMockMvc 어노테이션을 반드시 추가해 주어야 한다.


3

(3)에서 DI로 주입받은 MockMvc는 Tomcat 같은 서버를 실행하지 않고 Spring 기반의 애플리케이션의

Controller를 테스트할 수 있는 완벽한 환경을 지원해 주는 일종의 Spring MVC 테스트 프레임워크이다.

MockMvc 객체를 통해 우리가 작성한 Controller를 호출해서 손쉽게 Controller에 대한 테스트를 진행할 수 있다.


4

(1), (2), (3)을 통해 Controller를 테스트할 준비가 완료되었으니 이제 (4)와 같이

테스트하고자 하는 Controller 핸들러 메서드의 테스트 케이스를 작성하면 된다.


5

Postman을 사용해서 Controller에 요청을 하기 위해서는 request body 데이터가 필요하다.

Controller를 테스트하기 위해서는 (5)의 단계에서 테스트용 request body를 직접 만들어 주어야 한다.

Given-When-Then 패턴에서 Given에 해당된다.


6

(6)에서는 MockMvc 객체를 통해 요청 URI와 HTTP 메서드등을 지정하고,

(5)에서 만든 테스트용 request body를 추가한 뒤에 request를 수행한다.

Given-When-Then 패턴에서 When에 해당된다.


7

(7)에서는 Controller에서 전달받은 HTTP Status와 response body 데이터를 통해 검증 작업을 진행한다.

Given-When-Then 패턴에서 Then에 해당된다.

MemberController 테스트

HTTP Post request에 대한 테스트

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @Test
    void postMemberTest() throws Exception {
        // given (1)
        MemberDto.Post post = new MemberDto.Post("abc@gmail.com","demuu","010-1234-5678");
        String content = gson.toJson(post); // (2)

        // when
        ResultActions actions = mockMvc.perform( // (3)
                post("/v11/members") // (4)
                        .accept(MediaType.APPLICATION_JSON) // (5)
                        .contentType(MediaType.APPLICATION_JSON) // (6)
                        .content(content)
        );

        actions.andExpect(status().isCreated()) // (8)
                .andExpect(header().string("Location", is(startsWith("/v11/members/")))); // (9)
    }
}

위 코드는 MemberController의 postMember() 핸들러 메서드를 테스트하는 테스트 케이스이다.

Controller를 테스트하는 기본 구조에 테스트하고자하는 로직들을 포함시켰다.

Given

(1)의 코드는 Given에 해당되며 Postman을 사용할 때 request body에 포함시키는 요청 데이터와 동일한 역할을 한다.

(2)에서 Gson이라는 JSON 변환 라이브러리를 이용해서 (1)에서 생성한 MemberDto.Post 객체를

JSON 포맷으로 변환해준다.

Gson 라이브러리를 사용하기 위해서는 build.gradle dependencies에
implementation 'com.google.code.gson:gson'를 추가해 주어야 한다.


When

MockMvc로 테스트 대상 Controller의 핸들러 메서드에 요청을 전송하기 위해서는 기본적으로

(3)과 같이 perform() 메서드를 호출해야 하며 perform() 메서드 내부에 Controller 호출을 위한 세부적인 정보들이 포함된다.

(4)-(7) 까지는 HTTP request에 대한 정보이며, MockMvcRequestBuilders 클래스를 이용해서 빌더 패턴을 통해 request 정보를 채워 넣을 수 있다.

  • (4)에서 post() 메서드를 통해 HTTP POST METHOD와 request URL을 설정한다.
  • (5)에서 accept() 메서드를 통해 클라이언트 쪽에서 리턴 받을 응답 데이터 타입으로 JSON 타입을 설정한다.
  • (6)에서 contentType() 메서드를 통해 서버 쪽에서 처리 가능한 Content Type으로 JSON 타입을 설정한다.
  • (7)에서 content() 메서드를 통해 request body 데이터를 설정한다. request body에 전달하는 데이터는 (2)에서 Gson 라이브러리를 이용해 변환된 JSON 문자열이다.

Spring에서는 post()와 같이 HTTP METHOD에 해당하는 request를 수행하는 다양한 메서드를 지원한다. [ 참고 자료 ]


Then

MockMvc의 perform() 메서드는 ResultActions 타입의 객체를 리턴하는데,

이 ResultActions 객체를 이용해서 전송한 request에 대한 검증을 수행할 수 있다.

(8)에서 andExpect() 메서드를 통해 파라미터로 입력한 매처로 예상되는 기대 결과를 검증할 수 있다.

(8)에서는 status().isCreated() 를 통해 response status가 201(Created)인지 매치시키고 있다.

즉, 백엔드 측 리소스인 회원 정보가 잘 생성(저장)되었는지를 검증한다.

(9)에서 header().string("Location", is(startsWith("/v11/members/")))을 통해

HTTP header에 추가된 Location의 문자열 값이 “/v11/members/”로 시작하는지 검증한다.

Location header가 예상하는 값과 일치한다라는 것은 백엔드 측에 리소스(회원 정보)가 잘 생성되었다는 것을 의미한다.


이제 MemberController의 postMember()에 대한 기본적인 테스트가 이루어졌다.

postMember()의 경우, 클라이언트에게 되돌려주는 response body는 없기 때문에 위 코드와 같이

단순히 기대하는 response status와 Location header의 값만 테스트하면 된다.

MemberController에 대한 테스트 케이스를 하나만 더 작성해 보겠다.

이번에는 getMember() 핸들러 메서드를 테스트해 보겠다.

getMember()의 경우, 특정 회원 정보 조회를 위한 request가 MemberController 쪽에 잘 전송되는지

그리고 request를 전달받은 getMember()가 request에 해당하는 회원 정보를 response로 잘 전달하는지를 확인하면 된다.

getMember()가 특정 회원 정보를 response로 잘 전달하는지 테스트하려면

회원 정보 한 건을 먼저 백엔드 서버 측에 저장한 후,

방금 백엔드 서버 측에 저장한 리소스를 서버 측에 조회해서 조회가 잘 되는지 확인해 보면 된다.

@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    ...

    @Test
    void getMemberTest() throws Exception {
        // =================================== (1) postMember()를 이용한 테스트 데이터 생성 시작
        // given
        MemberDto.Post post = new MemberDto.Post("abc@gmail.com","demuu","010-1234-5678");
        String postContent = gson.toJson(post);

        ResultActions postActions = mockMvc.perform(
                post("/v11/members")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(postContent)
        );
        // =================================== (1) postMember()를 이용한 테스트 데이터 생성 끝

        // (2)
        String location = postActions.andReturn().getResponse().getHeader("Location"); // "/v11/members/1"

        // when / then
        mockMvc.perform(
                get(location) // (3)
                        .accept(MediaType.APPLICATION_JSON)
        ).andExpect(status().isOk()) // (4)
        .andExpect(jsonPath("$.data.email").value(post.getEmail())) // (5)
        .andExpect(jsonPath("$.data.name").value(post.getName())) // (6)
        .andExpect(jsonPath("$.data.phone").value(post.getPhone())); // (7)
    }
}

위 코드는 MemberController의 getMember() 핸들러 메서드를 테스트하는 테스트 케이스이다.

Given

(1)의 코드 영역은 이전 코드에서 postMember()를 테스트할 때의 코드와 동일하다.

(1)을 통해서 테스트 데이터를 백엔드 서버 측의 DB에 먼저 저장한다.

(2)에서는 postMember()의 response에 전달되는 Location header 값을 가져오는 로직이다.

(2)와 같이 postActions.andReturn().getResponse().getHeader("Location");로 접근해서 Location header의 값을 얻어올 수 있다.


When

(3)에서는 (2)에서 얻는 Location header의 값을 get(location)으로 전달한다.

Location header에서 얻게 되는 값이 (1)에서 등록한 resource(회원 정보)의 위치(”/v11/members/1”)를

의미하기 때문에 get(…)의 URI로 전달하면 된다.


Then

(4)에서는 기대하는 HTTP status가 200 OK 인지를 검증한다.

(5)에서 (7) 까지는 getMember() 핸들러 메서드에서 리턴하는 response body(JSON 형식)의

각 프로퍼티(email, name, phone)의 값을 검증하는 기능을 추가했다.

(5)에서는 jsonPath() 메서드를 통해 response body의 각 프로퍼티 중에서

응답으로 전달받는 email 값이 request body로 전송한 email과 일치하는지 검증하고 있다.

(6)에서는 jsonPath() 메서드를 통해 response body의 각 프로퍼티 중에서

응답으로 전달받는 name 값이 request body로 전송한 name과 일치하는지 검증하고 있다.

(7)에서는 jsonPath() 메서드를 통해 response body의 각 프로퍼티 중에서

응답으로 전달받는 phone 값이 request body로 전송한 phone과 일치하는지 검증하고 있다.

MockMvcResultMatchers와 jsonPath()에 대해서 더 알아보고 싶다면 아래 참고 자료를 참고하면 된다.

Response body 응답 데이터에 포함된 한글이 깨질 경우

만약 getMemberTest() 메서드 맨 아래쪽에

System.out.println(actions.andReturn().getResponse().getContentAsString()); 와 같은 코드를 추가해서

response body를 출력할 때, JSON 데이터에서 한글이 깨져 보일 경우, yml 파일에 아래의 설정을 추가한다.

server:
  servlet:
    encoding:
      force-response: true

지금까지 작성한 Controller 테스트 방법에는

Controller만 테스트하는 것이 아니라 애플리케이션의 전체 로직을 모두 실행하게 되는 문제점이 있다.

즉, 테스트에 집중해야 되는 계층은 API 계층인데 서비스 계층이나 데이터 액세스 계층까지 불필요한 로직이 수행된다.

따라서 완전한 슬라이스 테스트라고 보기에는 힘들다.

이 문제는 Mock(가짜) 객체를 사용해 계층 간의 연결을 끊어줌으로써 해결이 가능하다.

Mock 객체에 대한 내용은 Mockito 포스트에서 다루겠다. Mockito [더보기]

@WebMvcTest를 이용한 Controller 테스트

Spring에서는 Controller를 테스트하기 위한 전통적인 방법으로 @WebMvcTest 어노테이션을 사용할 수 있다.

하지만 @WebMvcTest 어노테이션을 사용할 경우,

Controller에서 의존하는 컴포넌트들을 모두 일일이 설정해 주어야 하는 불편함이 있다.

예를 들어 MemberController에서 사용되는 MemberService Bean, MemberMapper Bean

객체 등을 테스트 클래스에서 사용할 수 있도록 설정해 주어야 한다.

또한 때에 따라서 데이터 액세스 계층에서 의존하는 설정이나 의존 객체들도 모두 설정해 주어야 할 수도 있다.

이런 이유로 이 포스트에서는 @SpringBootTest, @AutoConfigureMockMvc를 이용해서

Controller 테스트를 위한 구성의 복잡함을 해결하고 있다.

데이터 액세스 계층 테스트

데이터 액세스 계층의 테스트 방법은 JPA와 어느 정도 밀접한 관련이 있다.

실무에서 대부분 JPA를 사용하기 때문에 이 방식만 알아도 큰 문제는 없지만

Spring JDBC나 Spring Data JDBC 등에 대한 테스트 방법 역시 지원하고 있다는 사실을 기억하면 좋다.

테이터 액세스 계층 테스트 시에는 아래와 같은 한 가지 규칙을 지키는 것이 좋다.

  • DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만든다.
public class DataAccessLayerTest {
    @Test
    public void testA() {
        // (1-1) 데이터가 DB에 잘 저장되는지를 테스트하기 위해 한 건의 데이터를 DB에 저장
        // (1-2) DB에 잘 저장되었는지 DB에서 조회해서 결과를 확인
    }

    @Test
    public void testB() {
        // (2-1) 데이터가 DB에서 잘 조회 되는지를 테스트하기 위해 DB에서 조회
    }
}

위 코드는 JUnit으로 데이터 액세스 계층을 테스트하는 이론적인 예이다.

일반적으로 데이터 액세스 계층을 테스트하기 위해 데이터베이스에 저장하는 테스트 데이터는

테스트 케이스를 실행할 때 대부분 같은 데이터로 테스트를 진행한다.

테스트 케이스마다 각각 다른 데이터를 매번 각각의 테스트 케이스에 입력으로

전달하는 건 비효율적이기 때문이다.

그리고 JUnit으로 작성한 테스트 케이스는 항상 일정한 순서로 테스트 케이스가 실행된다는 보장이 없다.

예를 들어, DataAccessLayerTest 클래스 내의 전체 테스트 케이스를 실행했더니,

다음과 같은 순서로 테스트 케이스가 실행된다고 가정해 보겠다.

testA() 실행

  • (1-1) 테스트 데이터 한 건을 DB에 저장한다.
  • (1-2) DB에 잘 저장되었는지 DB에서 조회하여 결과를 검증한다.

다음으로 testB() 실행

  • (2-1) 특정 데이터가 DB에서 잘 조회되는지 기본키를 WHERE 조건으로 해서 DB에서 조회한다.
  • 만약 testA()에서 INSERT 한 데이터의 기본키를 WHERE 조건으로 테스트하면 조회가 되므로 테스트 결과는 passed일 것이다.

그런데, 다시 테스트 케이스 전체를 실행했는데, 이번에는 실행 순서가 바뀌어서 testB()가 먼저 실행이 되었다.

이 경우에는 이미 테스트 케이스에 입력으로 전달한 WHERE 조건의 값이 고정된 상태에서

DB에 조회를 했는데 원하는 결과 값이 없기 때문에 테스트 결과는 failed일 것이다.

첫 번째 경우는 testA()가 먼저 실행이 되었기 때문에 테스트 결과가 passed이지만

두 번째 경우는 testA()가 먼저 실행이 되지 않았기 때문에 INSERT된 데이터가 없기 때문이다.

이처럼 테스트 케이스는 여러 개의 테스트 케이스를 일괄적으로 실행시키더라도 각각의 테스트 케이스에 독립성이 보장되어야 한다.

이러한 문제가 발생하지 않도록 하는 가장 좋은 방법은 테스트 케이스 하나가 실행될 때,

해당 테스트 케이스에서 사용했던 데이터가 DB에 저장이 되어 있는 상태라면 테스트 케이스 실행 종료 시점에 저장되었던 데이터를 삭제해 주는 것이다.

즉, DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만드는 것이다.

MemberRepository 테스트

회원 정보 저장 테스트

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest // (1)
public class MemberRepositoryTest {
    @Autowired
    private MemberRepository memberRepository; // (2)

    @Test
    public void saveMemberTest() {
        // given (3)
        Member member = new Member();
        member.setEmail("abc@gmail.com");
        member.setName("demuu");
        member.setPhone("010-1111-2222");

        // when (4)
        Member savedMember = memberRepository.save(member);

        // then (5)
        assertNotNull(savedMember); // (5-1)
        assertTrue(member.getEmail().equals(savedMember.getEmail()));
        assertTrue(member.getName().equals(savedMember.getName()));
        assertTrue(member.getPhone().equals(savedMember.getPhone()));
    }
}

위 코드에서는 MemberRepository의 save() 메서드가 잘 동작하는지 테스트하고 있다.

1

Spring에서 데이터 액세스 계층을 테스트하기 위한 가장 핵심적인 방법은 (1)과 같이 @DataJpaTest 어노테이션이다.

@DataJpaTest 어노테이션을 테스트 클래스에 추가함으로써,

MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 해주게 된다.

@DataJpaTest 어노테이션은 @Transactional 어노테이션을 포함하고 있기 때문에

하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리된다.

즉, 여러 개의 테스트 케이스를 한꺼번에 실행시켜도 하나의 테스트 케이스가 종료될 때마다

데이터베이스의 상태가 초기 상태를 유지한다는 것이다.


2

(2)에서 테스트 대상 클래스인 MemberRepository를 DI 받는다.


3

(3)에서 테스트할 회원 정보 데이터(member)를 준비한다.


4

(4)에서 회원 정보를 저장한다.


5

(5)에서 회원 정보가 잘 저장되었는지 검증(Assertion)한다.

  • 먼저 (5-1)과 같이 회원 정보를 정상적으로 저장한 뒤에 리턴 값으로 반환된 Member 객체(savedMember)가 null이 아닌지를 검증한다.
  • 나머지는 리턴 값으로 반환된 Member 객체(savedMember)의 필드들이 테스트 데이터와 일치 하는지 검증한다.

이처럼 Spring에서 데이터 액세스 계층의 기능을 테스트하는 건 어렵지 않다.

바로 @DataJpaTest라는 어노테이션이 있기 때문이다.

@DataJpaTest 어노테이션은 아래와 같은 자동 구성 기능들을 Import 해준다.

org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration 
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration 
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration 
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration 
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration 
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration 
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration 
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration 
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration 
org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration 
org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration

@DataJpaTest는 Spring Boot의 모든 자동 구성을 활성화하는 것이 아니라
데이터 액세스 계층에 필요한 자동 구성을 활성화한다.

회원 정보 조회 테스트

이번에는 MemberRepository에서 중복된 이메일 주소가 있는지를 조회하는 기능의

findByEmail(String email) 메서드가 조회를 잘하는지 테스트해 보도록 하겠다.

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
public class MemberRepositoryTest {
    ...
    ...

    @Test
    public void findByEmailTest() {
        // given (1)
        Member member = new Member();
        member.setEmail("abc@gmail.com");
        member.setName("demuu");
        member.setPhone("010-1111-2222");

        // when 
        memberRepository.save(member);  // (2)
        Optional<Member> findMember = memberRepository.findByEmail(member.getEmail()); // (3)

        // then (4)
        assertTrue(findMember.isPresent()); // (4-1)
        assertTrue(findMember.get().getEmail().equals(member.getEmail())); // (4-2)
    }
}

위 코드에서는 memberRepository의 save() 메서드가 잘 동작하는지 테스트하고 있다.

  1. (1)에서 테스트할 회원 정보 데이터(member)를 준비한다.

  2. (2)에서 회원 정보를 저장한다.

  3. 이번에는 (2)에서 저장 후, 리턴되는 Member 객체를 이용하는 것이 아니라 (2)에서 저장한 회원 정보 중에서 이메일에 해당되는 회원 정보를 잘 조회하는지 테스트하기 위해 (3)과 같이 findByEmail()로 회원 정보를 조회하고 있다.

  4. (4)에서 회원 정보의 조회가 정상적으로 이루어지는지 검증(Assertion)한다.

    • 먼저 (4-1)과 같이 조회된 회원 정보가 null이 아닌지를 검증한다.
    • (4-2)에서 조회한 회원의 이메일 주소와 테스트 데이터의 이메일과 일치하는지 검증한다.

Spring JDBC 환경에서는 @JdbcTest, Spring Data JDBC 환경에서는 @DataJdbcTest를
사용하면 데이터 액세스 계층에 대한 테스트를 진행할 수 있다.

실습

import static com.codestates.member.entity.Member.MemberStatus.MEMBER_SLEEP;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@Transactional
@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerHomeworkTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                "홍길동",
                "010-1234-5678");
        String content = gson.toJson(post);


        // when
        ResultActions actions =
                mockMvc.perform(
                        post("/v11/members")
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        // then
        actions
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v11/members/"))));
    }

    @Test
    void patchMemberTest() throws Exception {
        MemberDto.Post post = new MemberDto.Post(
                "abc@gmail.com", "demuu", "010-1234-1234");
        String content = gson.toJson(post);

        ResultActions actions = mockMvc.perform(
                post("/v11/members")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content)
        );

        long memberId;
        String location = actions.andReturn().getResponse().getHeader("Location");
        memberId = Long.parseLong(location.substring(location.lastIndexOf("/")+1));

        MemberDto.Patch patch = new MemberDto.Patch(memberId, "demung", "010-1111-2222", MEMBER_SLEEP);
        String patchContent = gson.toJson(patch);

        mockMvc.perform(
                patch("/v11/members/" + patch.getMemberId())
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(patchContent)
        ).andExpect(status().isOk())
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.phone").value(patch.getPhone()))
                .andExpect(jsonPath("$.data.memberStatus").value(patch.getMemberStatus().getStatus()));
    }

    @Test
    void getMemberTest() throws Exception {
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동","010-1111-1111");
        String postContent = gson.toJson(post);

        ResultActions postActions =
                mockMvc.perform(
                        post("/v11/members")
                                .contentType(MediaType.APPLICATION_JSON)
                                .accept(MediaType.APPLICATION_JSON)
                                .content(postContent)
                );
        long memberId;
        String location = postActions.andReturn().getResponse().getHeader("Location");
        memberId = Long.parseLong(location.substring(location.lastIndexOf("/") + 1));
        
        mockMvc.perform(
                        get("/v11/members/" + memberId)
                                .accept(MediaType.APPLICATION_JSON)
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andExpect(jsonPath("$.data.phone").value(post.getPhone()));
    }

    @Test
    void getMembersTest() throws Exception {
        MemberDto.Post post1 = new MemberDto.Post("user1@gmail.com", "demuu1", "010-1111-1111");
        MemberDto.Post post2 = new MemberDto.Post("user2@gmail.com", "demuu2", "010-2222-2222");

        String postContent1 = gson.toJson(post1);
        String postContent2 = gson.toJson(post2);

        mockMvc.perform(
                post("/v11/members")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(postContent1)
        ).andExpect(status().isCreated());

        mockMvc.perform(
                post("/v11/members")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(postContent2)
        ).andExpect(status().isCreated());

        mockMvc.perform(
                get("/v11/members")
                        .param("page", "1")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON)
        ).andExpect(status().isOk())
                .andExpect(jsonPath("$.data[0].email").value(post2.getEmail()))
                .andExpect(jsonPath("$.data[0].name").value(post2.getName()))
                .andExpect(jsonPath("$.data[0].phone").value(post2.getPhone()))
                .andExpect(jsonPath("$.data[1].email").value(post1.getEmail()))
                .andExpect(jsonPath("$.data[1].name").value(post1.getName()))
                .andExpect(jsonPath("$.data[1].phone").value(post1.getPhone()));

    }

    @Test
    void deleteMemberTest() throws Exception {
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동","010-1111-1111");
        String postContent = gson.toJson(post);

        ResultActions postActions = mockMvc.perform(
                post("/v11/members")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(postContent)
        ).andExpect(status().isCreated());


        long memberId;
        String location = postActions.andReturn().getResponse().getHeader("Location"); // "/v11/members/1"
        memberId = Long.parseLong(location.substring(location.lastIndexOf("/") + 1));

        mockMvc.perform(
                delete("/v11/members/"+memberId)
                        .accept(MediaType.APPLICATION_JSON)
        ).andExpect(status().isNoContent());
    }
}

추가 자료

스모크 테스트 [더보기]

MockMvc 기능들 [더보기]

MockMvc에서 지원하는 post()와 같이 request를 수행하는 메서드들 [더보기]

MockMvcResultMatchers와 jsonPath()

  1. [더보기]
  2. [더보기]
  3. [더보기]
  4. [더보기]

@DataJpaTest [더보기]

Spring Boot 핵심 정리 모음

1P - POJO, IoC, DI, AOP, PSA >

1.1P - DI / 의존성 주입은 왜 필요한가? >

2P - 빈(Bean) 생명주기와 범위 >

3P - 컴포넌트 스캔과 의존성 자동 주입, @Autowired >

4P - AOP (관심 지향 프로그래밍) >

5P - Spring MVC / MVC의 동작 방식과 구성 요소 >

6P - REST, REST API >

7P - HTTP Header, Rest Client >

8P - DTO, DTO 유효성 검증 >

9P - Service / Entity 클래스, Mapper / MapStruct >

10P - 예외 처리 (@ExceptionHandler, @RestControllerAdvice) >

10.1P - 비즈니스 로직에 대한 예외 처리 >

11P - JDBC 데이터 액세스 계층 >

11.1P - DDD, 도메인 엔티티 및 테이블 설계 >

11.2P - 데이터 액세스 계층 구현 (도메인 엔티티 클래스 정의) >

11.3P - 데이터 액세스 계층 구현 (서비스, 리포지토리 구현) >

12P - JPA(Java Persistence API) >

12.1P - JPA 엔티티 매핑 >

12.2P - JPA 엔티티 간의 연관 관계 매핑 >

12.3P - Spring Data JPA 데이터 액세스 계층 구현 >

13P - Spring MVC 트랜잭션 >

13.1P - 선언형 방식의 트랜잭션 적용 >

13.2P - JTA를 이용한 분산 트랜잭션 적용 >

14P - Spring MVC Testing 단위 테스트 >

14.1P - JUnit 단위 테스트 >

14.2P - Hamcrest Assertion >

14.3P - 슬라이스 테스트 (API, 데이터 액세스 계층) >

14.4P - Mockito >

14.5P - TDD (Test Driven Development) >

15P - API 문서화 (Documentation) >

15.1P - API 문서 생성, Asciidoc, Asciidoctor >

16P - 애플리케이션 빌드 / 실행 / 배포 >

17P - Spring Security >

17.1P - Spring Security 2 >

18P - JWT (JSON Web Token) >

댓글남기기