본문 바로가기
Spring boot/스프링 부트 3 백엔드 개발자 되기(자바편)

[4장] 스프링 부트 3와 테스트

by 리버🐦‍🔥 2023. 7. 25.

4.1 테스트 코드 개념 익히기

    4.1.1 테스트 코드란?

        - given-when-then 패턴

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

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

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

// given-when-then 패턴의 테스트 코드 예시
@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);
}

 

4.2 스프링 부트 3와 테스트

    - spring-boot-sstarter-test 스타터에는 테스트를 위한 도구들이 모여있음

더보기

<스프링 부트 스타터 테스트 목록>

- JUnit : 자바 프로그래밍 언어용 단위 테스트 프레임워크

- Spring Test & Spring Boot Test : 스프링 부트 애플리케이션을 위한 통합 테스트 지원

- AssertJ : 검증문인 어설션을 작성하는 데 사용되는 라이브러리

- Hamcrest : 표현식을 보다 이해하기 쉽게 만드는데 사용되는 Matcher 라이브러리

- Mockito : 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크

- JSONassert : JSON용 어설션 라이브러리

- JsonPath : JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리

(이 중 JUnit과 AssertJ를 가장 많이 사용한다)

    4.2.1 JUnit

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

더보기

<Junit의 특징>

-  테스트 방식을 구분할 수 있는 애너티에션을 제공

- @Test 애너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능

- 예상 결과를 검증하는 어설션 메서드 제공

- 사용 방법이 단순, 테스트 코드 작성 시간이 적음

- 자동 실행, 자체 결과를 확인하고 즉작적인 피드백을 제공

        <Junit으로 단위 테스트 코드 만들기>

        1단계. JUnitTest.java 파일 생성

            - @DisplayName : 테스트 이름 명시 기능

            - @Test : 해당 애너테이션이 붙은 메서드를 테스트 수행

            - 각 테스트를 실행할 때마다 실행 객체를 만들고, 종료와 동시에 삭제

            - assertEquels() : 첫 번째 인수 = 기대하는 값 / 두 번째 인수 = 실제로 검증할 값.

 

        2단계. 실행 확인

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(a + b, sum);    // 값이 같은지 확인
    }
}

▲ Test 실행 결과(성공)

        3단계. 실패 테스트 케이스

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

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

        Assertions.assertEquals(a + b, sum);
    }
}

▲ Test 실행 결과 (실패)

        4단계. 다양한 애너테이션 사용해보기

            - @BeforeAll : 전체 테스트를 시작하기 전에 처음으로 한 번만 실행 (한 번만 실행 -> static으로 선언 O)

                (ex. 데이터 베이스 연결, 테스트 환경 초기화)

 

            - @BeforeEach : 테스트 케이스를 시작하기 전에 매번 실행 (매번 실행 -> static으로 선언 X)

                (ex. 테스트 메서드에서 사용하는 객체 초기화, 테스트에 필요한 값 미리 넣을 때 사용)

 

            - @AfterAll : 전체 테스트를 마치고 종료하기 전에 한 번만 실행 (한 번만 실행 -> static으로 선언 O)

                (ex. 데이터베이스 연결을 종료할 때, 공통적으로 사용하는 자원을 해제할 때)

 

            - @AfterEach : 각 테스트 케이스를 종료하기 전에 매번 실행 (매번 실행 -> static으로 선언 X)

                (ex. 테스트 이후 특정 데이터를 삭제해야하는 경우 사용)

 

import org.junit.jupiter.api.*;

public class JUnitCycleTest {
    @BeforeAll  // 전체 테스트를 시작하기 전에 1회 실행하므로 메서드는 static으로 선언
    static void beforeAll() {
        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
클래스 레벨 설정
@BeforeEach
메서드 레벨 설정
@Test
테스트 실행
@AfterEach
메서드 레벨 정리
@AfterAll
클래스 레벨 정리

(@AfterEach -> @BeforeEach 테스트 개수만큼 반복)

▲ 테스트의 생명 주기

        - AssertJ로 검증문 가독성 높이기

// 서로 같은 코드
Assertion.assertEquals(a + b, sum);
//		▼
assertThat(a + b).isEqualTo(sum);
메서드 이름 설명 메서드 이름 설명
isEqualTo(A) A 값과 같은지 검증 isEmpty() 비어있는 값인지 검증
isNotEqualTo(A) A 값과 다른지 검증 isNotEmpty() 비어있지 않은 값인지 검증
contains(A) A 값을 포함하는지 검증 isPositive() 양수인지 검증
doesNoContain(A) A 값을 포함하지 않는지 검증 isNegative() 음수인지 검증
startsWith(A) 접두사가 A인지 검증 isGreaterThan(1) 1보다 큰 값인지 검증
endsWith(A) 접미사가 A인지 검증 isLessThan(1) 1보다 작은 값인지 검증

 

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

    1단계. TestControllerTest.java 파일 생성

    2단계. TestControllerTest.java 파일 작성

package me.kyungsoolee.springbootdeveloper;

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

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

@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를 생성하고 자동으로 구성

 

    3단계. 로직 테스트 코드 작성

package me.kyungsoolee.springbootdeveloper;


import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

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

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

        // when
        final ResultActions result = mockMvc.perform(get(url)   // (1) 요청을 전송하는 메서드
                .accept(MediaType.APPLICATION_JSON));   // (2) 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드

        // then
        result
                .andExpect(status().isOk())   // (3) 응답을 검증하는 메서드
                // (4) 응답 값을 가져오고, 응답의 0번째 값이 DB에서 저장한 값과 같은지 확인하는 메서드
                .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
                .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
    }
}

 

        - Given : 멤버를 저장

        - When : 멤버 리스트를 조회하는 API 호출

        - Then : 응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인

코드 매핑 메서드 설명
200 OK isOK() HTTP 응답 코드가 200 OK인지 검증
201 Created isCreated() HTTP 응답 코드가 201 Created인지 검증
400 Bad Request isBadRequest() HTTP 응답 코드가 400 Bad Request인지 검증
403 Forbidden isForbidden() HTTP 응답 코드가 403 Forbidden인지 검증
404 Not Found isNotFound() HTTP 응답 코드가 404 Not Found인지 검증
400번대 응답 코드 is4xxClientError() HTTP 응답 코드가 400번대 응답 코드인지 검증
500 Internal Server Error isInternalServerError() HTTP 응답 코드가 500 Internal Server Error인지 검증
500번대 응답 코드 is5xxServerError() HTTP응답 코드가 500번대 응답 코드인지 검증

    4단계. 실행 확인

▲ 실행 화면

<4장 핵심요약>
1. 테스트 코드를 작성여 기능을 검증할 수 있다.
    1-1. given : 테스트 준비
    1-2. when : 테스트 진행
    1-3. then : 테스트 결과 검증

2. JUnit는 단위 테스트를 할 때 사용하는 자바 테스트 프레임워크이다. -> @Before~@After 애너테이션 생성주기 공부!

3. AssertJ는 JUnit과 함께 사용하는 가독성을 높여주는 라이브러리이다.