본문 바로가기

SpringBoot

[스프링 부트 3 백엔드 개발자 되기] ch 4. 스프링 부트 3과 테스트

1. 테스트 코드 개념 익히기 

 

1) 테스트 코드

 

- 테스트 코드는 작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로 작성하는 코드

- test 디렉터리에서 작업한다. 

 

given-when-then 패턴 

- given은 테스트 실행을 준비하는 단계 

- when은 테스트를 진행하는 단계 

- then은 테스트 결과를 검증하는 단계 

 

ex) 새로운 메뉴를 저장하는 코드를 테스트 

@DisplayName("새로운 메뉴를 저장한다.")
@Test
public void saveMenuTest() {
    //given : 메뉴를 저장하기 위한 준비 과정
    final String name = "아메리카노";
    final int price = 2000;
    final Menu americano = new Menu(name, price);
    
    //when : 실제로 메뉴를 저장 
    final long savedId = menuService.save(americano);
    
    //then : 메뉴가 잘 추가되었는지 검증
    final Menu savedMenu = menuService.findById(savedId).get()
    assertThat(savedMenu.getName()).isEqualTo(name);
    assertThat(savedMenu.getPrice()).isEqualTo(price);
}

 

 

2. 스프링 부트 3과 테스트 

 

1) JUnit

 

- 자바 언어를 위한 단위 테스트 프레임워크 

- 단위 테스트란 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미한다. 이때 단위는 보통 메서드가 된다. 

 

JUnit의 특징

  • 테스트 방식을 구분할 수 있는 애너테이션을 제공
  • @Test 애너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능
  • 예상 결과를 검증하는 어설션 메서드 제공
  • 사용 방법이 단순, 테스트 코드 작성 시간이 적음
  • 자동 실행, 자체 결과를 확인하고 즉각적인 피드백을 제공 

 

JUnit으로 단위 테스트 코드 만들기 

 

- [scr->test->java] 폴더에 JUnitTest.java 파일을 생성 

 

JUnitTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JUnitTest {
    @DisplayName("1+2는 3이다") //테스트 이름
    @Test //테스트 메서드
    public void junitTest() {
        int a = 1;
        int b = 2;
        int sum = 3;

        Assertions.assertEquals(sum,a+b); //값이 같은지 확인
    }
}

 

- @DisplayName 애너테이션테스트 이름을 명시한다. @Test 애너테이션을 붙인 메서드는 테스트를 수행하는 메서드가 된다. 

- JUnit은 테스트끼리 영향을 주지 않도록 각 테스트를 실행할 때마다 테스트를 위한 실행 객체를 만들고 테스트가 종료되면 실행 객체를 삭제한다. 

 

- 이 테스트에서는 JUnit에서 제공하는 검증 메서드 assertEquals()로 a+b와 sum의 값이 같은지 확인한다. 

 

- JUnitTest 파일을 우클릭하여 테스트 실행 

 

 

- 실패용 테스트 케이스를 실행하면 테스트가 실패했다는 표시와 함께 기댓값과 실제로 받은 값을 비교해서 알려준다. 

이렇게 JUnit은 테스트 케이스가 하나라도 실패하면 전체 테스트를 실패한 것으로 보여준다. 

 

 

 

JUnitCycleTest.java

import org.junit.jupiter.api.*;

public class JUnitCycleTest {

    @BeforeAll
    static void beforeAll() { //전체 테스트를 실행하기 전에 1회 실행하므로 메서드는 static으로 선언 
        System.out.println("@BeforeAll");
    }

    @BeforeEach //테스트 케이스를 시작하기 전마다 실행 
    public void beforeEach() {
        System.out.println("@BeforeEach");
    }

    @Test
    public void test1() {
        System.out.println("test1");
    }

    @Test
    public void test2() {
        System.out.println("test2");
    }

    @Test
    public void test3() {
        System.out.println("test3");
    }

    @AfterAll //전체 테스트를 마치고 종료하기 전에 1회 실행하므로 메서드는 static으로 선언 
    static void afterAll() {
        System.out.println("@AfterAll");
    }

    @AfterEach //테스트 케이스를 종료하기 전마다 실행 
    public void afterEach() {
        System.out.println("@AfterEach");
    }

}

 

 

@BeforeAll 애너테이션

 

- 전체 테스트를 시작하기 전에 처음으로 한 번만 실행한다. 

- 예를 들어 데이터베이스를 연결해야 하거나 테스트 환경을 초기화할 때 사용한다. 

- 이 애너테이션은 전체 테스트 실행 주기에서 한 번만 호출되어야 하기 때문에 메서드를 static으로 선언해야 한다. 

 

@BeforeEach 애너테이션

 

- 테스트 케이스를 시작하기 전에 매번 실행한다. 

- 예를 들어 테스트 메서드에서 사용하는 객체를 초기화하거나 테스트에 필요한 값을 미리 넣을 때 사용할 수 있다. 

- 각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 한다. 

 

@AfterAll 애너테이션

 

- 전체 테스트를 마치고 종료하기 전에 한 번만 실행한다. 

- 예를 들어 데이터베이스 연결을 종료할 때나 공통적으로 사용하는 자원을 해제할 때 사용할 수 있다. 

- 전체 테스트 실행 주기에서 한 번만 호출되어야 하므로 메서드를 static으로 선언해야 한다. 

 

@AfterEach 애너테이션

 

- 각 테스트 케이스를 종료하기 전 매번 실행한다. 

- 예를 들어 테스트 이후에 특정 메서드를 삭제해야 하는 경우 사용한다. 

- 메서드는 static이 아니어야 한다. 

 

 

- @BeforeAll 애너테이션으로 설정한 메서드가 실행되고, 그 이후에는 테스트 케이스 개수만큼 @BeforeEach -> @Test -> @AfterEach의 생명주기로 테스트가 진행된다. 모든 테스트 케이스가 끝나면 @AfterAll 애너테이션으로 설정한 메서드를 실행하고 종료한다. 

 

 

AssertJ로 검증문 가독성 높이기 

 

- AssertJ는 JUnit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리이다. 

 

ex) 기댓값과 비교값이 잘 구분되지 않는 Assertion 예

Assertions.assertEquals(sum,a+b);

 

ex) 가독성이 좋은 AssertJ 예

- a와 b를 더한 값이 sum과 같아야 한다는 의미 

assertThat(a+b).isEqualTo(sum)

 

 

메서드 이름 설명
isEqualTo(A) A 값과 같은지 검증
isNotEuqalTo(A) A 값과 다른지 검증
contains(A) A 값을 포함하는지 검증
doesNotContain(A) A 값을 포함하지 않는지 검증 
startsWith(A) 접두사가 A인지 검증
endsWith(A) 점미사가 A인지 검증
isEmpty() 비어 있는 값인지 검증
isPositive() 양수인지 검증
isNegative() 음수인지 검증
isGreaterThan(1) 1보다 큰 값인지 검증
isLessThan(1) 1보다 작은 값인지 검증 

 

 

테스트 코드 작성 연습 문제 

 

문제 1)

 

- String으로 선언한 변수 3개가 있다. 여기에서 세 변수 모두 NULL이 아니며 name1과 name2는 같은 값을 가지고, name3는 다른 나머지 두 변수와 다른 값을 가지는 데, 이를 검증하는 테스트 작성 

 

JUnitQuiz.java

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;

public class JUnitQuiz {

    @Test
    public void junitQuiz1() {
        String name1 = "홍길동";
        String name2 = "홍길동";
        String name3 = "홍길은";

        // 모든 변수가 null이 아닌지 확인
        assertThat(name1).isNotNull();
        assertThat(name2).isNotNull();
        assertThat(name3).isNotNull();

        // name1과 name2가 같은지 확인
        assertThat(name1).isEqualTo(name2);

        // name1과 name3이 다른지 확인
        assertThat(name1).isNotEqualTo(name3);
    }
}

 

 

 

문제 2)

 

- int로 선언된 변수 3개가 있다. number1, number2, number3은 각각 15, 0, -1의 값을 가진다. 세 변수가 각각 양수,0,음수이고 number1은 number2보다 큰 값이고, number3는 number2보다 작은 값임을 검증하는 테스트 

 

JUnitQuiz.java

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;

public class JUnitQuiz {

    @Test
    public void junitQuiz2() {
        int number1 = 15;
        int number2 = 0;
        int number3 = -5;

        //number1은 양수인지 확인
        assertThat(number1).isPositive();

        //number2는 0인지 확인
        assertThat(number2).isZero();

        //number3은 음수인지 확인
        assertThat(number3).isNegative();

        //number1은 number2보다 큰지 확인
        assertThat(number1).isGreaterThan(number2);

        //number3은 number2보다 작은지 확인
        assertThat(number3).isLessThan(number2);
    }
}

 

 

 

 

3. 제대로 테스트 코드 작성해보기 

 

- TestController.java 파일을 열고 클래스 이름 위에 마우스 커서를 놓고 클릭한 다음 Alt+Enter을 누르면 [Create Test]가 나타난다. [Create Test]창이 열리고 [OK]를 누르면 TestControllerTest.java 파일이 test/java/패키지 아래에 생성된다. 

 

테스트 클래스 단축키 - 클래스 명 선택 or 클래스 내부에 커서 둔 채로 [ Ctrl + Shift + T ] 누르기

 

TestControllerTest.java

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
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;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@SpringBootTest // 테스트용 애플리케이션 컨텍스트 생성
@AutoConfigureMockMvc // MockMvc 생성 및 자동구성
class TestControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private MemberRepository memberRepository;

    @BeforeEach // 테스트 실행 전 실행하는 메소드
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @AfterEach // 테스트 실행 후 실행하는 메소드
    public void cleanUp() {
        memberRepository.deleteAll();
    }
}

 

 

@SpringBootTest

 

- 메인 애플리케이션 클래스에 추가하는 애너테이션인 @SpringBootApplication이 있는 클래스를 찾고 그 클래스에 포함되어 있는 빈을 찾은 다음 테스트용 애플리케이션 컨텍스트라는 것을 만든다. 

 

@AutoConfigureMockMvc

 

- MockMvc를 생성하고 자동으로 구성하는 애너테이션

- MockMvc는 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송,응답 기능을 제공하는 유틸리티 클래스 

- 즉, 컨트롤러를 테스트할 때 사용되는 클래스 

 

@BeforeEach

 

- MockMvcSetUp() 메서드를 실행해 MockMvc를 설정해준다. 

 

@AfterEach

 

- cleanUp() 메서드를 실행해 member 테이블에 있는 데이터들을 모두 삭제해준다. 

 

 

- TestController의 로직을 테스트하는 코드 작성 

 

TestControllerTest.java

...
 @DisplayName("getAllMembers: 아티클 조회에 성공한다.")
    @Test
    public void getAllMembers() throws Exception {
        // given - 멤버를 저장
        final String url = "/test";
        Member savedMember = memberRepository.save(new Member(1L, "홍길동"));

        // when - 멤버 리스트를 조회하는 API 호출
        final ResultActions result = mockMvc.perform(get(url) // 1
                .accept(MediaType.APPLICATION_JSON)); // 2

        // then- 응답코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인
        result
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
                .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
    }
}

 

- lombok은 게터(getter), 세터(setter), 생성자와 같이 클래스를 만들 때 반복하여 입력하는 코드를 줄여주기 위한 라이브러리 

- 인텔리제이에서 lombok을 사용하려면 lombok 플러그인도 다운로드해야 한다. 

 

perform() 메서드 

 

- 요청을 전송하는 역할을 하는 메서드 

- 결과로 ResultActions 객체를 받으며, ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드를 제공해준다. 

 

accept() 메서드 

 

- 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드 

 

andExpect() 메서드 

 

- 응답을 검증

- TestController에서 만든 API는 응답으로 OK(200)을 반환하므로 이에 해당하는 메서드인 isOK를 사용해 응답 코드가 OK(200)인지를 확인 

 

jsonPath("$[0].${필드명}")

 

-  JSON 응답값의 값을 가져오는 역할을 하는 메서드이다. 0번째 배열에 들어 있는 객체의 id,name값을 가져오고, 저장된 값과 같은지 확인 

 

 

 

 

테스트 코드 패턴 연습하기 

 

QuizController.java

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class QuizController {
    @GetMapping("/quiz") //1
    public ResponseEntity<String> quiz(@RequestParam("code") int code) {
        switch(code){
            case 1:
                return ResponseEntity.created(null).body("Created!");

            case 2:
                return ResponseEntity.badRequest().body("Bad Request!");

            default:
                return ResponseEntity.ok().body("OK!");
        }
    }

    @PostMapping("/quiz") //2
    public ResponseEntity<String> quiz2(@RequestBody Code code) {
        switch(code.value()) {
            case 1 :
                return ResponseEntity.status(403).body("Forbidden!");
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }
}

record Code(int value) {} //3

 

 

1) 

- quiz 패스로 GET 요청이 오면 quiz()라는 메서드에서 요청을 처리한다. 

- 이 메서드는 요청 파라미터의 키가 "code"이면 int 자료형의 code 변수와 매핑되며, code 값에 따라 다른 응답을 보낸다. 

 

2)

- quiz 패스로 POST 요청이 오면 quiz2()라는 메서드에서 요청을 처리한다. 

- 이 메서드는 요청 값을 Code라는 객체로 매핑한 후value 값에 따라 다른 응답을 보낸다

 

3)

- 매핑할 객체로 사용하기 위해 선언한 레코드

- 데이터 전달을 목적으로 하는 객체를 더 빠르고 편하게 만들기 위한 기능으로 레코드를 사용하면 필드, 생성자, 게터 , equals(), hashCode(), toString() 메서드 등을 자동으로 생성한다. 

 

 

QuizControllerTest.java

@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

}

 

- ObjectMapper는 Jackson 라이브러리에서 제공하는 클래스로 객체와 JSON 간의 변환을 처리해준다. 

 

ex)

Code code = new Code(13)
objectMapper.writeValueAsString(code)

 

- new Code(13)과 같이 객체를 만들고 writeValueAsString(code)와 같이 메서드를 호출하면 JSON 형태의 문자열로 객체가 변환된다. 이를 객체 직렬화라고 한다. 

 

{'value' :13}

 

 

문제 

 

- GET 요청을 보내 응답 코드마다 예상하는 응답을 반환하는지 검증하는 테스트 

 

 

QuizControllerTest.java


...
@DisplayName("quiz(): GET /quiz?code = 1이면 응답 코드는 201, 응답 본문은 Created!를 리턴한다.")
    @Test
    public void getQuiz1() throws Exception {
        //given
        final String url = "/quiz";

        //when
        final ResultActions result = mockMvc.perform(get(url).param("code","1")
        );

        //then
        result.andExpect(status().isCreated())
                .andExpect(content().string("Created!"));
    }

    @DisplayName("quiz(): GET /quiz?code=2이면 응답코드는 400, 응답 본문은 Bad Request!를 리턴한다.")
    @Test
    public void getQuiz2() throws Exception {
        //given
        final String url = "/quiz";

        //when
        final ResultActions result = mockMvc.perform(get(url).param("code","2"));

        //then
        result
                .andExpect(status().isBadRequest())
                .andExpect(content().string("Bad Request!"));
    }

}