백엔드/Spring Boot

4. 스프링 부트 3와 테스트

두개의 문 2023. 7. 14. 17:16

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

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

 

 

1) 테스트 코드란?

 

 ▪︎ 테스트 코드 : test 디렉터리에서 작업 

 ▪︎ 테스트 코드의 다양한 패턴 중 given-when-then 패턴 이용 

  → 테스트 코드를 세 단계로 구분해 작성하는 방식 

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

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

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

 

 


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

 ◉ 스프링 부트는 애플리케이션을 테스트하기 위한 도구와 애너테이션을 제공함 

 

▪︎ spring-boot-starter-test 스타터의 테스트 도구 목록 

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

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

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

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

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

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

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

 

 

1) JUnit이란? 

 

  ▪︎ JUnit : 자바 언어를 위한 단위 테스트 프레임 워크 

    - 단위 테스트 : 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미 ( 보통 단위로 메서드가 됨 )

  ▪︎ JUnit특징 

    ❶ 테스트 방식을 구분할 수 있는 애너테이션을 제공 

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

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

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

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

 

   ⇒ 테스트끼리 영향을 주지 않도록, 각 테스트를 실행할 때마다 테스트를 위한 실행 객체를 만들고 테스트 종료 시 실행 객체를 삭제함 

 

 

 

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

 

① src/test/java 폴더에 'JUnitTest.java' 파일 생성 

package org.choongang;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset;
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);	
	}
    
    	// 테스트가 실패한 경우 
	@DisplayName("1 + 3는 4이다")
	@Test 
	public void junitFailedTest() {
		int a = 1;
		int b = 3;
		int sum = 3;
		
		// 예측값과 계산값이 달라 실패하는 케이스 
		Assertions.assertEquals(a + b, sum);
	}
}

 ▪︎ @DisplayName : 테스트 이름 명시 

 ▪︎ @Test : 테스트를 수행하는 메서드임을 알려줌 

 

 ▪︎ junitTest( ) 메서드에 작성한 테스트 코드 설명 

  - Assertions 클래스 : JUnit에서 제공하는 검증 메서드인 다양한 종류의 assert 메서드가 존재 

    →  그 중 assertEquals( )로 a + b와 sum의 값이 같은지 확인

        ( 첫 번째 인수 : 기대하는 값, 두 번째 인수 : 실제로 검증할 값 입력 )

 

 

 

② 실제로 테스트 코드가 잘 동작하는지 확인해보자 

   ※ IntelliJ의 경우, unit 단위로 테스트 가능 / 콘솔창에 테스트 결과 출력

       이클립스의 경우, 메서드 클릭 후 오른쪽 클릭 - Run As - JUnit Test 실행 가능 

 ▪︎ 테스트 결과, 성공 여부 및 테스트 케이스의 이름, 테스트 실행 시간 정보 확인 가능 

   → JUnit Test 창에서 색으로 표시됨 ( 성공 :  녹색 / 실패 : 갈색 / 에러 : 빨간색 )

       단, 테스트 케이스가 하나라도 실패 , 전체 테스트를 실패한 것으로 보여줌 

 

 

 

③ 자주 사용하는 JUnit 애너테이션에 대해 알아보자

 - JUnit : 각 테스트에 대해 객체를 만들어 독립적으로 실행 

 - 테스트 : 애너테이션에 따라 실행 순서가 정해짐 

 

 ▪︎ JUnitCycleTest.java 생성 

package org.choongang;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class JUnitCycleTest {
	@BeforeAll
	// 전체 테스트를 시작하기 전에 1회 실행 → 메서드는 static으로 선언 
	static void beforeAll() {
		System.out.println("@BeforeAll - 전체 테스트 중 제일 처음 실행");
	}
	
	@BeforeEach
	// 테스트 케이스를 시작하기 전마다 실행 
	public void beforeEach() {
		System.out.println("@BeforeEach - 이 메서드 실행 후, test 메서드 실행");
	}
	
	@Test
	public void test1() {
		System.out.println("test1 실행 중...");
	}
	
	@Test
	public void test2() {
		System.out.println("test2 실행 중...");
	}
	@Test
	public void test3() {
		System.out.println("test3 실행 중...");
	}
	
	@AfterEach
	// 테스트 케이스를 종료하기 전마다 실행 
	public void afterEach() {
		System.out.println("@AfterEach - test 메서드 실행 후, 이 메서드 실행");
	}
	
	@AfterAll
	// 전체 테스트를 마치고 종료하기 전에 1회 실행 → 메서드는 static으로 선언 
	static void afterAll() {
		System.out.println("@AfterAll - 전체 테스트 중 제일 마지막에 실행");
	}
}

▪︎ 실행하기 전, 다양한 애너테이션 설명

  ❶ @BeforeAll

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

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

    - 전체 테스트 주기에서 한번만 호출되어야 함 → stati으로 선언 

 

  ❷ @BeforeEach

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

    - 테스트 메서드에서 사용하는 객체를 초기화하거나 테스트에 필요한 값을 미리 넣을 때 사용 

    - 각 인스턴스에 대해 메서드를 호출해야 하므로 static으로 선언하지 않음 

 

  ❸ @AfterAll

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

    - 데이터베이스 연결을 종료할 때나 공통적으로 사용하는 자원을 해제할 때 사용 

    - 전체 테스트 주기에서 한번만 호출되어야 함 → stati으로 선언 

 

  ❹ @AfterEach

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

    - 테스트 이후에 특정 데이터를 삭제해야 하는 경우 사용 

    - @BeforeEach와 마찬가지로 static으로 선언하지 않음 

 

 

 

④ 테스트 코드를 실행해서 출력 결과를 살펴보자 

▪︎ 애너테이션을 중심으로 JUnit의 실행 흐름 

@BeforeEach   →   @BeforeEach   →   @Test   →   @AfterEach   →   @AfterAll 

클래스 레벨 설정       메서드 레벨 설정                              메서드 레벨 설정      클래스 레벨 설정 

                                       ⎣           테스트의 개수만큼 반복           

 

⇒ @Test 로 선언된 테스트 메서드만 확인됨 / 콘솔창에서는 전체 확인 가능 

 

 


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

 

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

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

 ▪︎ 앞에서 작성한 테스트 코드의 Assertion은 기대값과 실제값을 명시하지 않으므로, 비교 대상이 헷갈리는 경우가 있음 

// 기댓값과 비교값이 잘 구분되지 않는 Assertion 예
Assertions.assertEquals( a + b, sum );

 

 ▪︎ 대규모 프로젝트에서는 더 명확한 코드를 이용해 실수를 줄여 가독성을 높여야 함 

    이럴 경우, AssertJ를 적용해보자 

// 가독성이 좋은 AssertJ 예
assertThat( a + b ).isEqualTo( sum );

    → a와 b를 더한 값이 sum과 같아야 한다는 의미로 명확하게 읽힘 

 

 ▪︎ AssertJ의 다양한 메서드 

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

 

 


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

 

① TestController.java 파일에 대한 test 파일 생성 

 ▪︎ 이클립스 : 해당 컨트롤러 - 오른쪽 클릭 - New - Other - test 입력 후, JUnit Test Case 클릭

                   → 컨트롤러와 동일한 이름에 Test가 붙은 java 파일 생성 

 

 ▪︎ TestControllerTest.java

package org.choongang.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.choongang.domain.Member;
import org.choongang.domain.MemberRepository;
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;

@SpringBootTest		// 테스트용 애플리케이션 컨텍스트 생성 
@AutoConfigureMockMvc	// MockMvc 생성 
class LayerControllerTest {

	@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 즉, 같은 파일의 @AfterEach 이후에 코드 추가 작성 

	// ------------------- 테스트메서드 -------------------
	@DisplayName("getAllMembers : 아티클 조회에 성공한다.")
	@Test
	public void getAllMembers() throws Exception{
		// given : 멤버를 저장 
		final String url = "/hello";
		Member savedMember = memberRepository.save(new Member(1L, "홍길동"));
		
		// when : 멤버 리스트를 조회하는 API를 호출 
		final ResultActions result = mockMvc.perform(get(url)
			.accept(MediaType.APPLICATION_JSON)); 
		
		// then : 응답코드가 200 OK이고, 반환 받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인 
		result 
			.andExpect(status().isOk())
			.andExpect(jsonPath("$[0].id").value(savedMember.getId()))
			.andExpect(jsonPath("$[0].name").value(savedMember.getName()));
				
	}

 

▪︎ Given - When -Then 패턴 적용 

 ① given : 멤버 저장 

   - url 및 모델변수인 memberRepository에 새로운 멤버 생성해 저장 ( 테스트용 데이터 )

  

 ② when : 멤버 리스트를 조회하는 API를 호출 

   - mockMvc의 perform( ) 메서드 : 요청을 전송하는 역할 → 결과로 ResultActions 객체 반환 

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

     → JSON, XML 등 다양한 타입 존재 

 

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

   - 위에서 perform( ) 메서드의 결과, ResultActions 객체 반환

       ResultActions 객체의 andExpect( ) 메서드 : 응답을 검증 

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

   - jsonPath("$[0].${필드명}") : JSON 응답값의 값을 가져오는 역할을 하는 메서드 

    → 0번째 배열에 들어있는 객체의 Id, name 값을 가져오고, 저장된 값과 같은지 확인

 

 

 

③ 실제로 테스트가 잘 동작하는지 확인해보자 

 

 

◉ HTTP 주요 응답 코드 

코드  매핑 메서드 설명
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번대 응답 코드인지 검증 

 

 

 


 - 테스트 코드를 작성하면 코드의 기능이 제대로 작동한다는 것을 검증 가능

    ( 보통 테스트를 준비하는 given / 테스트를 실제로 진행하는 when / 테스트 결과를 검증하는 then )

 - JUnit : 단위 테스트를 할 때 사용하는 자바 테스트 프레임 워크 

    @BeforeAll 애너테이션으로 설정한 메서드가 실행되고,

     그 이후에는 테스트 케이스의 개수만큼 '@BeforeEach → @Test → @AfterEach'의 생명주기를 가지고 실행함 

    모든 테스트가 완료되면 마지막으로 @AfterAll 애너테이션으로 설정한 메서드가 실행되고 종료됨 

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