1. 테스트 코드
1.1. 테스트 코드란?
: 작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로 작성하는 코드
- 장점1) 유지보에 좋음
- 장점2) 코드 수정 시 기존 기능이 제대로 작동하지 않을까봐 걱정하지 않아도 됨
1.2. 테스트코드의 다양한 패턴
given-when-then 패턴
- 테스트 코드를 세 단계로 구분해 작성하는 방식
@DisplayName("새로운 메뉴를 저장한다.")
@Test
public void saveMenuTest(){
// 1) given : 메뉴를 저장하기 위한 준비 과정
final String name = "아메리카노";
final int price = 2000;
final Menu americano = new Menu(name, price);
// 2) when : 실제로 메뉴를 저장
final long saveId = menuService.save(americano);
// 3) then : 메뉴가 잘 추가되었는지 검증
final Menu = saveMenu = menuService.findById(saveId).get();
assertThat(saveMenu.getName()).isEqualTo(name);
assertThat(saveMenu.getPrice()).isEqualTo(price);
}
1) given : 테스트 실행을 준비하는 단계
ex. 메뉴를 저장하기 위해 준비하는 과정
2) when : 테스트를 진행하는 단계
ex. 실제로 메뉴를 저장하는 과정
3) then : 테스트 결과를 검증하는 단계
ex. 메뉴가 잘 추가되었는지 검증하는 과정
ex. 새로운 메뉴를 저장하는 코드를 테스트 하는 상황
2. 스프링 부트 3와 테스트
- spring-boot-starter-test 스타터 : 애플리케이션 테스트를 위해 스프링 부트가 제공하는 도구와 애너테이션
*JUnit | 자바 프로그래밍 언어용 단위 테스트 프레임워크 |
Spring Test & Spring Boot Test | 스프링 부트 애플리케이션을 위한 통합 테스트 지원 |
*AssertJ | 검증문인 어설션을 작성하는 데 사용되는 라이브러리 |
Hamcrest | 표현식을 이해하기 쉽게 만드는 데 사용되는 Matcher 라이브러리 |
Mockito | 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크 |
JSONassert | JSON용 어설션 라이브러리 |
JsonPath | JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리 |
* : 가장 많이 사용하는 도구
3. JUnit
- JUnit : 자바 언어를 위한 단위 테스트 프레임워크
- 단위테스트 : 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것 (단위는 보통 메서드)
- JUnit 사용 시 단위 테스트를 작성하고 테스트하는 데 도움을 줌
- 장점1) 사용법 간단
- 장점2) 테스트 결과가 직관적
- 특징1) 테스트방식을 구분할 수 있는 애너테이션을 제공
- 특징2) @Test 애너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립테스트 가능
- 특징3) 예상 결과를 검증하는 어설션 메서드 제공
- 특징4) 사용방법이 단순, 테스트 코드 작성 시간이 적음
- 특징5) 자동 실행, 자체 결과를 확인하고 즉각적인 피드백을 제공
3.1. 성공한 테스트 케이스
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class JUnitTest {
// 4장 스프링부트3와 테스트
// Junit은 테스트끼리 영향을 주지 않도록 각 테스트를 실행할때마다 테스트를 위한 실행 객체를 만들고
// 테스트가 종료되면 실행 객체를 삭제
@DisplayName("1+2는 3이다") // 테스트 이름
@Test // 테스트를 수행하는 메서드
public void junitTest(){
int a = 1;
int b = 2;
int sum = 3;
//assertEquals() : Junit에서 제공하는 검증 메서드
// 첫번째 인수 : 기대하는값
// 두번째 인수 : 실제로 검증할 값
Assertions.assertEquals(sum, a+b);// 값이 같은 지 확인
}
}
-> Run JUnit Test을 실행하여 테스트 진행
3.2. 실패한 테스트 케이스
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class JUnitTest {
// 4장 스프링부트3와 테스트
// Junit은 테스트끼리 영향을 주지 않도록 각 테스트를 실행할때마다 테스트를 위한 실행 객체를 만들고
// 테스트가 종료되면 실행 객체를 삭제
@DisplayName("1+2는 3이다") // 테스트 이름
@Test // 테스트를 수행하는 메서드
public void junitTest(){
int a = 1;
int b = 2;
int sum = 3;
//assertEquals() : Junit에서 제공하는 검증 메서드
// 첫번째 인수 : 기대하는값
// 두번째 인수 : 실제로 검증할 값
Assertions.assertEquals(sum, a+b);// 값이 같은 지 확인
}
@DisplayName("1+3는 4이다") // 테스트 이름
@Test // 테스트를 수행하는 메서드
public void junitFailedTest(){
int a = 1;
int b = 3;
int sum = 3;
Assertions.assertEquals(sum, a+b); // 실패하는 케이스
}
}
-> JUnit은 테스트 케이스가 하나라도 실패하면 전체 테스트를 실패한 것으로 보여줌
3.3. 자주 사용하는 JUnit 애너테이션
- JunitFailedTest() 메서드 삭제
- JUnitCycleTest.java 파일 생성
//JUnitCycleTest.java 파일
import org.junit.jupiter.api.*;
public class JUnitCycleTest {
@BeforeAll // 전체 테스트를 시작하기 전에 1회 실행하므로 메서드는 static으로 선언
static void beforeAll(){
System.out.println("@BeforeAll");
}
@BeforeEach // 테스트 케이스를 시작하기 전마다 실행
public void befordEach(){
System.out.println("@BeforeEach");
}
@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");
}
}
- JUnit : 각 테스트에 대해 객체를 만들어 독립적으로 실행
- 테스트는 애너테이션에 따라 실행 순서 정해짐
1) @BeforeAll
- 전체 테스트를 시작하기 전에 처음으로 한 번만 실행
- 전체 테스트 실행 주기에서 한 번만 호출
-> 메서드를 static 으로 선언해야 함
ex.1 데이터베이스 연결
ex. 테스트 환경 초기화
2) @BeforeEach
- 테스트 케이스를 시작하기 전에 매번 실행
ex. 테스트 메서드에서 사용하는 객체를 초기화하거나 테스트에 필요한 값을 미리 넣을 때 사용
- 각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함
3) @AfterAll
- 전체 테스트를 마치고 종료하기 전에 한 번만 실행
ex. 데이터베이스 연결을 종료할 때
ex. 공통적으로 사용하는 자원을 해제할 때
- 전체 테스트 실행 주기에서 한 번만 호출
-> 메서드를 static 으로 선언해야 함
4) @AfterEach
- 테스트 케이스를 종료하기 전 매번 실행
ex. 테스트 이후에 특정 데이터를 삭제해야 하는 경우 사용
- static이 아니어야 함
애너테이션을 중심으로 JUnit의 실행 흐름을 살펴봄
test2() 메서드 실행 결과
test3() 메서드 실행 결과
- @ BeforeAll
-> 테스트 개수만큼 @BeforeEach - @Test - @AfterEach의 생명주기로 테스트 진행
->모든 테스트 케이스가 끝나면 @AfterAll 애너테이션으로 설정한 메서드를 실행하고 종료
4. AssertJ
- AssertJ는 Junit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리
ex. 앞서 작성한 테스트 코드의 Assertion은 기댓값과 실제 비교값을 명시X
-> 비교대상 헷갈림
ex.
Assertions.assertEquals(sum, a + b);
=> 기댓값과 비교값이 잘 구분되지 않음
- 가독성은 대규모 프로젝트에서는 조금 더 명확한 모습의 코드가 실수를 줄일 수 있어 꽤 중요한 문제!
=> AssertJ 적용
ex. AssertJ적용
assertThat(a + b).isEqualTo(sum);
=> 'a와 b를 더한 값이 sum 과 같아야한다' 라는 의미로 명확하게 읽힘
-> 코드 읽는 사람이 헷갈리지 않음
자주 사용하는 AssertJ 메서드
메서드 이름 | 설명 |
isEqulaTo(A) | A 값과 같은지 검증 |
isNotEqualTo(A) | A 값과 다른지 검증 |
contains(A) | A 값을 포함하는지 검증 |
doesNotContain(A) | A 값을 포함하지 않는지 검증 |
startsWith(A) | 접두사가 A인지 검증 |
endsWith(A) | 접미사가 A인지 검증 |
isEmpty() | 비어있는 값인지 검증 |
isNotEmpty() | 비어있지 않은 값인지 검증 |
isPossitive() | 양수인지 검증 |
isNegative() | 음수인지 검증 |
isGreaterThan(1) | 1보다 큰 값인지 검증 |
isLessThan(1) | 1보다 작은 값인지 검증 |
TestController.java 파일의 TestController 클래스명에 alt+enter를 하면 메뉴바가 나오면서 Create test 를 할 수 있다.
나온 화면에서 ok를 누르면
test 폴더 의 하위 구성으로 testControllerTest.java 파일이 생성된다.
//TestControllerTest.java
package org.example.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 // MocMvc 생성 및 자동 구성
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();
}
}
1) @SpringBootTest
메인 애플리케이션 클래스에 추가하는 애너테이션인 @SpringBootApplication이 있는 클래스를 찾고
->그 클래스에 포함되어 있는 빈을 찾은 다음
-> 테스트용 애플리케이션 컨텍스트라는 것을 만듦
2) @AutoConfigureMockMvc
- MockMvc를 생성하고 자동으로 구성하는 애너테이션
- MockMvc : 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스
== 컨트롤러를 테스트할 때 사용되는 클래스
3) @BeforeEach
: 테스트를 실행하기 전 실행하는 메서드에 적용하는 애너테이션
ex. MockMvcSetUp() 메서드를 실행해 MockMvc를 설정해줌
4) @AfterEach
- 테스트를 실행한 이후에 실행하는 메서드에 적용하는 애너테이션
ex. cleanUp() 메서드 실행해 member테이블에 있는 데이터들을 모두 삭제해줌
//TestControllerTest.java
package org.example.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.junit.jupiter.api.Assertions.*;
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;
// 2단계
@SpringBootTest // 테스트용 애플리케이션 컨텍스트 생성
@AutoConfigureMockMvc // MocMvc 생성 및 자동 구성
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();
}
// 3단계
@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) perform() : 요청을 전송하는 역할을 하는 메서드 ->결과로 ResultActions 객체를 받음. (ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드 제공
.accept(MediaType.APPLICATION_JSON)); // 2) accept() : 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드 (JSON, XML... 다양! 여기서는 JSON받도록 명시)
// then : 응답코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인
result
.andExpect(status().isOk()) //3) andExpect() : 응답을 검증함
// ex. TestController에서 만든 API는 응답으로 OK(200)을 반환 -> 이에 해당하는 메서드인 isOk를 사용해 응답코드가 OK(200)인지 확인
// 4) 응답의 0번째 값이 DB에 저장한 값과 같은지 확인
// jsonPath("$[0].${필드명}") : JSON 응답값을 가져오는 역할을 하는 메서드
// 0번째 배열에 들어있는 객체의 id, name값을 가져오고, 저장된 값과 같은지 확인
.andExpect(jsonPath("$[0].id").value(savedMember.getId()))
.andExpect(jsonPath("$[0].name").value(savedMember.getName()));
}
}
<Given - When - Then 패턴>
- Given : 멤버를 저장
- When : 멤버 리스트를 조회하는 API를 호출
- Then : 응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인
1) perform() : 요청을 전송하는 역할을 하는 메서드
->결과로 ResultActions 객체를 받음.
(ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드 제공)
2) accept() : 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드
(JSON, XML... 다양! ->여기서는 JSON받도록 명시)
3) andExpect() : 응답을 검증함
ex. TestController에서 만든 API는 응답으로 OK(200)을 반환 -> 이에 해당하는 메서드인 isOk를 사용해 응답코드가 OK(200)인지 확인
4) jsonPath("$[0].${필드명}") : JSON 응답값을 가져오는 역할을 하는 메서드
ex. 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인지 검증 |
- 테스트 실행 방법 : 파일 우클릭 - Run TestController
<정리>
1. 테스트 코드 작성 시 -> 코드의 기능이 제대로 작동한다는 것을 검증 가능
- 테스트는 보통 3단계로 구분
1) given : 테스트를 준비
2) when : 테스트를 실제로 진행
3) then : 테스트 결과를 검증
ex. 새로운 메뉴를 데이터베이스에 저장하는 로직 given-when-then 패턴으로 나눠 적기
- Given : 저장할 새로운 메뉴 정보를 생성
- When : 저장할 새로운 메뉴 정보를 저장
- Then : 저장된 메뉴 정보가 Given 절에서 준ㅂ니한 메뉴 정보와 같은지 검증
2. JUnit : 단위 테스트를 할 때 사용하는 자바 테스트 프레임워크
- @BeforeAll 애너테이션으로 설정한 메서드 실행 -> 테스트 케이스 개수만큼 @BeforeEach -> @Test -> @AfterEach 의 생명주기 가지고 실행 -> - 모든 테스트 완료되면 마지막으로 @AfterAll 애너테이션으로 설정한 메서드 실행되고 종료
3. AssertJ : JUnit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리
ex.
Assertions.assertEquals(sum, a + b);
이 코드에 assertJ 도입?
->
assertThat(sum).isEqualTo(a+b);
ex. HTTP응답코드가 201 Created이고, 반환받은 값 중 0번째 요소의 id가 15 인지 검증하는 코드
result // ResultActions
.andExpect(status().isCreated())
.andExpect(jsonPath("$[0].id").values(15))
며칠동안 공부했던 테스트가 끝났다. 매번 개발을 할때 일일이 syso로 찍어보고 서버를 재실행하고 번거로움이 많았었는데, 이 테스트 도구들을 활용하면 간편한 연산이나 조건에 따른 값을 간단하게 확인해보기에는 유용할 것 같다. 처음 접해보는 개념이라 친해질 시간이 필요할 것 같은데, 드디어 JUnit이 뭔지 알게 되어서 후련하다.
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 11. ORM(Object-Relational Mapping), JPA(Java Persistence API), Hibernate, Spring Data JPA (0) | 2024.08.26 |
---|---|
[Spring Boot] 9. 스프링 부트 3 구조 살펴보기 (0) | 2024.08.20 |
[Spring Boot] 8. 스프링 부트 3 코드 이해하기 (0) | 2024.08.20 |
[Spring Boot] 7. 스프링 부트3 과 자바 버전 (2) | 2024.08.19 |
[Spring Boot] 6. 스프링 부트 자동 구성 (0) | 2024.08.19 |