Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

Develog

코드스테이츠 55일차 본문

코드스테이츠

코드스테이츠 55일차

안형준 2022. 7. 13. 19:07

학습 목표

  • Hamcrest를 사용해야 하는 이유를 이해할 수 있다.
  • Hamcrest의 기본 사용법을 이해할 수 있다.


Hamcrest란?
Hamcrest는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework이다.

Hamcrest를 사용하는 이유

  • Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상 된다.
  • 테스트 실패 메시지를 이해하기 쉽다.
  • 다양한 Matcher를 제공한다.


JUnit Assertion을 사용한 단위 테스트에 Hamcrest Assertion 적용해보기
✅ JUnit에서의 Assertion

import org.junit.jupiter.api.DisplayName;

import org.junit.jupiter.api.Test;



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



public class HelloJunitTest {

    @DisplayName("Hello Junit Test")

    @Test

    public void assertionTest1() {

        String actual = "Hello, JUnit";

        String expected = "Hello, JUnit";



        assertEquals(expected, actual); // (1)

    }

}

✅ Hamcrest에서의 Assertion

import org.junit.jupiter.api.DisplayName;

import org.junit.jupiter.api.Test;



import static org.hamcrest.MatcherAssert.assertThat;

import static org.hamcrest.Matchers.equalTo;

import static org.hamcrest.Matchers.is;



public class HelloHamcrestTest {



    @DisplayName("Hello Junit Test using hamcrest")

    @Test

    public void assertionTest1() {

        String expected = "Hello, JUnit";

        String actual = "Hello, JUnit";



        assertThat(actual, is(equalTo(expected)));  // (1)

    }

}
  • JUnit Assertion 기능 이용

ㄴassertEquals(expected, actual);
ㄴㄴ파라미터로 입력된 값의 변수 이름을 통해 대략적으로 어떤 검증을 하려는지 알 수 있으나 구체적인 의미는 유추를 하는 과정이 필요하다.

  • Hamcrest의 매쳐(Matcher) 이용

ㄴassertThat(actual, is(equalTo(expected)));
ㄴㄴ(1)의 Assertion 코드 한 줄은 ‘assert that actual is equal to expected’라는 하나의 영어 문장으로 자연스럽게 읽혀진다.
ㄴㄴㄴ한글로 번역하자면, ‘결과 값(actual)이 기대 값(expected)과 같다는 것을 검증(Assertion)한다.’ 정도로 해석할 수 있다.
ㄴㄴassertThat() 메서드의 파라미터
ㄴㄴㄴ첫 번째 파라미터는 테스트 대상의 실제 결과 값이다.
ㄴㄴㄴ두 번째 파라미터는 기대하는 값이다. 즉, 이런 값일거라고 기대(예상)하는 값이다.

Junit → Hamcrest 예 2
JUnit에서의 Assertion

import org.junit.jupiter.api.DisplayName;

import org.junit.jupiter.api.Test;



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

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



public class HelloJunitTest {



    @DisplayName("Hello Junit Test")

    @Test

    public void assertionTest1() {

        String actual = "Hello, JUnit";

        String expected = "Hello, World";



        assertEquals(expected, actual);

    }

}

JUnit 테스트에서의 “failed” 예이다.
테스트 케이스 실행 결과는 “failed”이다.

위 코드의 실행결과는?

expected: <Hello, World> but was: <Hello, JUnit>

Expected :Hello, World

Actual   :Hello, JUnit


✅ Hamcrest에서의 Assertion

import org.junit.jupiter.api.DisplayName;

import org.junit.jupiter.api.Test;



import static org.hamcrest.MatcherAssert.assertThat;

import static org.hamcrest.Matchers.*;



public class HelloHamcrestTest {



    @DisplayName("Hello Junit Test using hamcrest")

    @Test

    public void assertionTest() {

        String expected = "Hello, World";

        String actual = "Hello, JUnit";



        assertThat(actual, is(equalTo(expected)));

    }

}

위 코드는 Hemcrest의 매쳐(Matcher)를 사용했으며, 테스트 케이스 실행 결과는 역시 “failed”이다.

위 코드의 실행결과는?

Expected: is "Hello, World"

     but: was "Hello, JUnit"

이처럼 Hemcrest의 Matcher를 사용해서 사람이 읽기 편한 자연스러운 Assertion 문장을 구성할 수 있으며, 실행 결과가 “failed”일 경우 역시 자연스러운 “failed” 메시지를 확인할 수 있기때문에 가독성이 상당히 높아진다.

JUnit → Hamcrest 예 3
JUnit에서의 Assertion

import org.junit.jupiter.api.DisplayName;

import org.junit.jupiter.api.Test;



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



public class AssertionNullHamcrestTest {



    @DisplayName("AssertionNull() Test")

    @Test

    public void assertNotNullTest() {

        String currencyName = getCryptoCurrency("ETH");



        assertNotNull(currencyName, "should be not null");

    }



    private String getCryptoCurrency(String unit) {

        return CryptoCurrency.map.get(unit);

    }

}

위 코드는 JUnit의 assertNotNull()을 이용해서 ETH 암호 화폐가 map에 존재하는지(null이 아닌지) 테스트 하고 있다.

✅ Hamcrest에서의 Assertion
import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class AssertionNullHamcrestTest {

@DisplayName("AssertionNull() Test")
@Test
public void assertNotNullTest() {
String currencyName = getCryptoCurrency("ETH");

assertThat(currencyName, is(notNullValue())); // (1)
// assertThat(currencyName, is(nullValue())); // (2)
}

private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit);
}
}
Hamcrest를 사용해서 Not Null 테스트를 하기위해서는 (1)과 같이 Hamcrest의 `is()`, `notNullValue()` 매쳐를 함께 사용할 수 있다.
‘currencyName is not Null Value.’와 같이 가독성 좋은 하나의 문장처럼 구성이 되는 것을 볼 수 있다.

만약 (2)를 주석 해제하면, 아래와 같은 “failed” 메시지를 확인할 수 있다.

Expected: is null

     but: was "Ethereum"

JUnit → Hamcrest 예 4
✅ JUnit에서의 Assertion

import com.codestates.CryptoCurrency;

import org.junit.jupiter.api.DisplayName;

import org.junit.jupiter.api.Test;



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



public class AssertionExceptionTest {



    @DisplayName("throws NullPointerException when map.get()")

    @Test

    public void assertionThrowExceptionTest() {

        assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));

    }



...

...



    private String getCryptoCurrency(String unit) {

        return CryptoCurrency.map.get(unit).toUpperCase();

    }

}

위 코드는 JUnit의 assertThrows()를 이용해서 XRP 암호 화폐가 map에 존재하는지(null이 아닌지) 여부를 던져지는 예외(Exception) 발생 여부로 테스트 하고 있다.

✅ Hamcrest에서의 Assertion

import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class AssertionExceptionHamcrestTest {

@DisplayName("throws NullPointerException when map.get()")
@Test
public void assertionThrowExceptionTest() {
Throwable actualException = assertThrows(NullPointerException.class,
() -> getCryptoCurrency("XRP")); // (1)

assertThat(actualException.getCause(), is(equalTo(null))); // (2)
}

private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit).toUpperCase();
}
}


위 코드는 Hamcrest를 사용해서 발생한 예외가 null인지 여부를 체크하고 있다.
그런데, 예외에 대한 테스트는 Hamcrest 만으로 Assertion을 구성하기 힘들기 때문에 (1)과 같이 JUnit의 assertThrows() 메서드를 이용해서 assertThrows()의 리턴 값으로 전달 받은 Exception 내부의 정보를 가져와서 assertThat(expectedException.getCause(), is(equalTo(null)));처럼 추가로 검증을 진행 했다.
만약 Hamcrest 만으로 던져진 예외를 테스트 하기 위해서는 Custom Matcher를 직접 구현해서 사용할 수 있다.
실행 결과는 (1)에서 1차적으로 NullPointException이 발생하므로 (1)의 Assertion 결과는 “passed”이고, (2)에서 결과 값인 actualException.getCause()가 null 이므로, (2)의 Assertion 결과 역시 “passed”이다.

총정리

  • Hamcrest는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework이다.
  • Hamcrest는 다음과 같은 이유로 JUnit에 지원하는 Assertion 메서드 보다 더 많이 사용된다.

ㄴAssertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상 된다.
ㄴ테스트 실패 메시지를 이해하기 쉽다.
ㄴ다양한 Matcher를 제공한다.

  • Hamcrest 만으로 던져진(thrown) 예외를 테스트 하기 위해서는 Custom Matcher를 직접 구현해서 사용할 수 있다.


학습 목표

  • 슬라이스 테스트(Slice Testing)의 의미를 이해할 수 있다.
  • Spring에서 지원하는 테스팅 기능을 이용해서 API 계층의 Controller를 테스트 할 수 있다.
  • Spring에서 지원하는 테스팅 기능을 이용해서 데이터 액세스 계층의 Repository를 테스트 할 수 있다.


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

개발자가 통합 테스트까지 작성하면 정말 바람직하겠지만 현실에서는 통합 테스트는 QA 부서에서 진행하는 기능 테스트로 대체되는 경우가 많다.
그리고 통합 테스트는 아니지만 QA 부서에서 본격적으로 전체적인 기능 테스트를 진행하기 전에 애플리케이션의 특정 수정 사항으로 인해 영향을 받을 수 있는 범위에 한해서 제한된 테스트를 진행하기도 한다. 이를 스모크 테스트(Smoke Test)라고 부른다.

API 계층 테스트
Controller 테스트를 위한 테스트 클래스 구조

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.web.servlet.MockMvc;



@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 객체들이 등록되어 있다.

 

  1. (2)의 @AutoConfigureMockMvc 애너테이션은 Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해준다.
    Spring Boot의 자동 구성을 통해 애플리케이션의 설정을 손쉽게 사용하듯이 @AutoConfigureMockMvc 애너테이션을 추가함으로써 테스트에 필요한 애플리케이션의 구성이 자동으로 진행된다.
    (3)의 MockMvc 같은 기능을 사용하기 위해서는 @AutoConfigureMockMvc 애너테이션을 반드시 추가해 주어야 한다.

 

  1. (3)에서 DI로 주입 받은 MockMvc는 Tomcat 같은 서버를 실행하지 않고 Spring 기반 애플리케이션의 Controller를 테스트 할 수 있는 완벽한 환경을 지원해주는 일종의 Spring MVC 테스트 프레임워크이다.
    MockMvc 객체를 통해 우리가 작성한 Controller를 호출해서 손쉽게 Controller에 대한 테스트를 진행할 수 있다.

 

  1. 이제 (4)와 같이 테스트 하고자 하는 Controller 핸들러 메서드의 테스트 케이스를 작성하면 된다.

 

  1. Postman을 사용해서 Controller에 요청을 하기 위해서는 reqeust body 데이터가 필요하다. Controller를 테스트 하기 위해서는 (5)의 단계에서 테스트용 request body를 직접 만들어 주어야 한다.
  2. Given-When-Then 패턴에서 Given에 해당된다.

 

  1. (6)에서는 MockMvc 객체를 통해 요청 URI와 HTTP 메서드등을 지정하고, (5)에서 만든 테스트용 request body를 추가한 뒤에 request를 수행한다.
  2. Given-When-Then 패턴에서 When에 해당된다.

 

  1. (7)에서는 Controller에서 전달 받은 HTTP Status와 response body 데이터를 통해 검증 작업을 진행한다.
  2. Given-When-Then 패턴에서 Then에 해당된다.


MemberController 테스트

import com.codestates.member.dto.MemberDto;

import com.google.gson.Gson;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.http.MediaType;

import org.springframework.test.web.servlet.MockMvc;

import org.springframework.test.web.servlet.MvcResult;

import org.springframework.test.web.servlet.ResultActions;



import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;



@SpringBootTest

@AutoConfigureMockMvc

class MemberControllerTest {

    @Autowired

    private MockMvc mockMvc;



    @Autowired

    private Gson gson;



    @Test

    void postMemberTest() throws Exception {

        // given  (1)

        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",

                                                        "홍길동",

                                                    "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)   // (7)

                                );



        // then

        MvcResult result = actions

                                .andExpect(status().isCreated()) // (8)

                                .andReturn();  // (9)



        // System.out.println(result.getResponse().getContentAsString());

    }

}
  1. Given
  • (1)의 코드는 Given에 해당되며 Postman을 사용할 때 request body에 포함시키는 요청 데이터와 동일한 역할을 한다.
  • (2)에서 Gson이라는 JSON 변환 라이브러리를 이용해서 (1)에서 생성한 MemberDto.Post 객체를 JSON 포맷으로 변환 해준다.

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

  1. 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를 수행하는 다양한 메서드를 지원한다.

  1. Then
  • MockMvc의 perform() 메서드는 ResultActions 타입의 객체를 리턴하는데, 이 ResultActions 객체를 이용해서 우리가 전송한 request에 대한 검증을 수행할 수 있다.
  • (8)에서 andExpect() 메서드를 통해 파라미터로 입력한 매처(Matcher)로 예상되는 기대 결과를 검증할 수 있다.
    (8)에서는 status().isCreated()를 통해 response status가 201(Created)이 맞는지 검증하고 있다.
  • (9)에서 andReturn()을 통해서 response 데이터를 확인할 수 있는데, 디버깅 용도로 response로 전달되는 응답 데이터를 출력할 때 사용할 수 있다. 일반적으로는 (8)까지의 검증 과정에서 테스트는 끝나게 된다.


✅ response body 응답 데이터에 포함된 한글이 깨질 경우
JSON 데이터에서 한글이 깨져 보일 경우, application.yml 파일에 아래의 설정을 추가하자

...

...



server:

  servlet:

    encoding:

      force-response: true

하지만 단순히 기대하는 response status가 검증에 통과한다고해서 Controller에 대한 테스트가 완벽하게 “passed”라고 생각하기에는 찜찜하다.
이 경우에는 Controller에서 결과 값으로 되돌려 주는 response body 데이터를 검증해 보는 것이 제일 확실한 검증 방법일 것이다.

import com.codestates.member.dto.MemberDto;

import com.google.gson.Gson;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.http.MediaType;

import org.springframework.test.web.servlet.MockMvc;

import org.springframework.test.web.servlet.MvcResult;

import org.springframework.test.web.servlet.ResultActions;



import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;



@SpringBootTest

@AutoConfigureMockMvc

class MemberControllerTest2 {

    @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

        MvcResult result = actions

                                .andExpect(status().isCreated())

                                .andExpect(jsonPath("$.data.email").value(post.getEmail()))  // (1)

                                .andExpect(jsonPath("$.data.name").value(post.getName()))   // (2)

                                .andExpect(jsonPath("$.data.phone").value(post.getPhone())) // (3)

                                .andReturn();



        System.out.println(result.getResponse().getContentAsString());

    }

}
  1. (1)에서는 jsonPath() 메서드를 통해 response body(JSON 형식)의 각 프로퍼티 중에서 응답으로 전달 받는 email 값이 request body로 전송한 email과 일치하는지 검증하고 있다.
  2. (2)에서는 jsonPath() 메서드를 통해 response body(JSON 형식)의 각 프로퍼티 중에서 응답으로 전달 받는 name 값이 request body로 전송한 name과 일치하는지 검증하고 있다.
  3. (3)에서는 jsonPath() 메서드를 통해 response body(JSON 형식)의 각 프로퍼티 중에서 응답으로 전달 받는 phone 값이 request body로 전송한 phone과 일치하는지 검증하고 있다.


MockMvcResultMatchers 클래스에서 지원하는 jsonPath()를 사용하면 JSON 형식의 개별 프로퍼티에 손쉽게 접근할 수 있다는 사실을 기억하자

하지만 위와같이 코드를 작성한다면 Controller만 테스트하는 것이 아니라 애플리케이션의 전체 로직을 모두 실행하게 된다.
즉, 우리가 테스트에 집중해야 되는 계층은 API 계층인데 서비스 계층이나 데이터 액세스 계층까지 불필요한 로직이 수행된다는 것이다.
이 문제는 Mock(가짜) 객체를 사용해 계층 간의 연결을 끊어줌으로써 해결이 가능하다.

@WebMvcTest를 이용한 Controller 테스트
Spring 에서는 Controller를 테스트 하기 위한 전통적인 방법으로 @WebMvcTest 애너테이션을 사용할 수 있다.
하지만 @WebMvcTest 애너테이션을 사용할 경우, Controller에서 의존하는 컴포넌트들을 모두 일일이 설정해 주어야 하는 불편함이 있다.
예를 들어 MemberController에서 사용되는 MemberService Bean, MemberMapper Bean 객체 등을 테스트 클래스에서 사용할 수 있도록 설정해 주어야 한다.
또한 때에 따라서 데이터액세스 계층에서 의존하는 설정이나 의존 객체들도 모두 설정해 주어야 할 수도 있다.

총정리

  • 개발자가 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서(Slice) 테스트하는 것을 슬라이스 테스트(Slice Test)라고 한다.
  • @SpringBootTest 애너테이션은 Spring Boot 기반의 애플리케이션을 테스트 하기 위한 Application Context를 생성한다.
  • @AutoConfigureMockMvc 애너테이션은 Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해준다.
  • MockMvc는 Tomcat 같은 서버를 실행하지 않고 Spring 기반 애플리케이션의 Controller를 테스트할 수 있는 완벽한 환경을 지원해주는 일종의 Spring MVC 테스트 프레임워크이다.
  • MockMvc로 테스트 대상 Controller의 핸들러 메서드에 요청을 전송하기 위해서는 기본적으로 perform() 메서드를 먼저 호출해야 한다.
  • MockMvcRequestBuilders 클래스를 이용해서 빌더 패턴을 통해 request 정보를 채워 넣을 수 있다.
  • MockMvc의 perform() 메서드가 리턴하는 ResultActions 타입의 객체를 이용해서 request에 대한 검증을 수행할 수 있다.


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

  • 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으로 작성한 테스트 케이스는 항상 일정한 순서로 테스트 케이스가 실행된다는 보장이 없다.
예를 들어, DataAccessLayerTest 클래스 내의 전체 테스트 케이스를 실행했더니, 다음과 같은 순서로 테스트 케이스가 실행 된다고 가정해보자
testA() 실행

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

다음으로 testB() 실행

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

그런데, 다시 테스트 케이스 전체를 실행했는데, 이번에는 실행 순서가 바뀌어서 testB()가 먼저 실행이 되었다면?
이미 테스트 케이스에 입력으로 전달한 WHERE 조건의 값이 고정된 상태에서 DB에 조회를 했는데 원하는 결과 값이 없기때문에 테스트 결과는 “failed”일 것이다.
이처럼 테스트 케이스는 여러 개의 테스트 케이스를 일괄적으로 실행 시키더라도 각각의 테스트 케이스에 독립성이 보장되어야 한다.
이러한 문제가 발생하지 않도록 하는 가장 좋은 방법은 테스트 케이스 하나가 실행될 때, 해당 테스트 케이스에서 사용했던 데이터가 DB에 저장이 되어 있는 상태라면 테스트 케이스 실행 종료 시점에 저장되었던 데이터를 삭제해 주는 것이다.
즉, DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만드는 것이다.

MemberRepository 테스트
✅ 회원 정보 저장 테스트

import com.codestates.member.entity.Member;

import com.codestates.member.repository.MemberRepository;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;



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("hgd@gmail.com");

        member.setName("홍길동");

        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()));

    }

}

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

  1. Spring에서 데이터 액세스 계층을 테스트 하기 위한 가장 핵심적인 방법은 바로 (1)과 같이 @DataJpaTest 애너테이션이다.
    @DataJpaTest 애너테이션을 테스트 클래스에 추가함으로써, MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 해주게 된다.
    @DataJpaTest 애너테이션은 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리 된다.
    즉, 여러 개의 테스트 케이스를 한꺼번에 실행 시켜도 하나의 테스트 케이스가 종료될 때마다 데이터베이스의 상태가 초기 상태를 유지한다는 것이다.

 

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

 

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

 

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

 

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


@DataJpaTest은 데이터 액세스 계층에 필요한 자동 구성을 활성화 한다는 사실을 기억하자

✅ 회원 정보 조회 테스트

import com.codestates.member.entity.Member;

import com.codestates.member.repository.MemberRepository;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;



import java.util.Optional;



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



@DataJpaTest

public class MemberRepositoryTest {

    ...

...



    @Test

    public void findByEmailTest() {

        // given (1)

        Member member = new Member();

        member.setEmail("hgd@gmail.com");

        member.setName("홍길동");

        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)를 준비한다.

 

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

 

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

 

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


Spring JDBCSpring Data JDBC 환경에서 테스트 환경을 손쉽게 구성할 수 있는 방법은 없을까?
Spring JDBC, @JdbcTest, Spring Data JDBC 환경에서는 @DataJdbcTest를 사용하면 손쉽게 데이터 액세스 계층에 대한 테스트를 진행할 수 있다.

총정리

  • 데이터 액세스 계층 테스트 시에는 테스트 종료 직 후, DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만든다.
  • @DataJpaTest 애너테이션을 사용하면 Spring Data JPA 환경에서 데이터 액세스 계층의 테스트를 손쉽게 진행할 수 있다.
  • @DataJpaTest 애너테이션은 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리된다.

'코드스테이츠' 카테고리의 다른 글

코드스테이츠 57일차  (0) 2022.07.15
코드스테이츠 56일차  (2) 2022.07.14
코드스테이츠 54일차  (0) 2022.07.12
코드스테이츠 53일차  (0) 2022.07.11
코드스테이츠 52일차  (0) 2022.07.09