leehyeon-dv 님의 블로그
스프링 부트와 테스트 본문
✨목차
- 테스트 코드란?
- 스프링 부트3와 테스트
- JUnit을 실제로 사용해보기
- AssertJ로 검증문 가독성 높이기
- 테스트 코드작성 연습문제 풀어보기
- 제대로 테스트 코드 작성해보기
- 테스트 코드 패턴 연습하기
- 테스트 코드 연습문제 풀어보기
1.📌테스트 코드란?
테스트 코드는 작성한 코드가 의도대로 잘 작동하고 예상하지 못한 문제가 없는지 확인할 목적으로 작성하는 코드이다
테스트 코드의 여러 패턴중 given-when-then 패턴을 알아보자
- 테스트코드를 세 단계로 구분해 작성하는 방식
- given = 테스트 실행을 준비하는 단계
- when = 테스트를 진행하는 단계
- then = 테스트 결과를 검증하는 단계
예) 새로운 메뉴를 저장하는 코드 테스트
@DisplayName("새로운 메뉴를 저장한다.")
@Test
public void saveMenuTest() {
//메뉴 저장하기 위한 준비과정
final String name = "아메리카노";
final int price = 2000;
final Menu americano = new Menu(name, price);
//실제 메뉴 저장
final long saveId = menuService.save(americano);
//메뉴 잘 추가됐는지 검증
final Menu saveMenu = menuService.finById(saveId).get();
assertThat(saveMenu.getName()).isEqualTo(name);
assertThat(saveMenu.getPrice()).isEqualTo(price);
}
2.📌스프링 부트3와 테스트
스프링 부트는 애플리케이션을 테스트하기 위한 도구와 애너테이션을 제공한다
spring-boot-starter-test 스타터에 테스트를 위한 도구가 있다
스프링부트 스타터 테스트 목록 | |
JUnit | 자바 프로그래밍 언어용 단위 테스트 프레임워크 |
Spring Test & Spring Boot Test | 스프링 부트 애플리케이션을 위한 통합 테스트 지원 |
AssertJ | 검증문인 어설션을 작성하는데 사용되는 라이브러리 |
Hamcrest | 표현식을 이해하기 쉽게 만드는데 사용되는 Matcher라이브러리 |
Mockito | 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고 관리하고 검증할 수 있게 지원하는 테스트 프레임워크 |
JSONassert | JSON용 어설션 라이브러리 |
JsonPath | JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리 |
- JUnit = 자바언어를 위한 단위 테스트 프레임워크
- 단위테스트 = 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것 (단위 = 메서드)
- @Test로 메서드를 호출시 마다 새 인스턴스를 생성, 독립테스트 가능
- 예상 결과를 검증하는 어설션 메서드 제공
- 사용방법이 단순, 테스트 코드 작성시간이 적음
- 자동실행, 자체 결과를 확인하고 즉각적인 피드백 가능
3.📌JUnit을 실제로 사용해보기
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 = a + b;
Assertions.assertEquals(sum,a+b);
}
}
@DisplayName은 테스트 이름을 명시한다
@Test가 붙으면 테스트를 수행하는 메서드가 된다
assertEqulas()는 첫번째인수 = 기대하는값, 두번째인수 = 실제로 검증할 값
🔻테스트 동작 잘되는지 확인(체크 누르면 테스트 실행시간 정보 확인가능)
🔻테스트가 실패할 경우
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class JUnitTest {
@DisplayName("1+2는 4이다")
@Test
public void junitFailTest(){
int a = 1;
int b = 3;
int sum = 3;
Assertions.assertEquals(sum,a+b);
}
}
JUnit은 테스트 케이스가 하나라도 실패하면 전체 테스트를 실패한 것으로 보여준다
🔻JUnit이 각 테스트에 대해 객체를 만들어서 독립적으로 실행하는지 확인
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
static void afterAll(){
System.out.println("@AfterAll");
}
@AfterEach //테스트 케이스를 종료하기 전마다 실행
public void afterEach(){
System.out.println("@AfterEach");
}
}
- @BeforeAll = db를 연결해야하거나 테스트 환경을 초기화 할때 사용
- @BeforeEach = 테스트 메서드에서 사용하는 객체 초기화, 테스트에 필요한 값 미리 넣을때 사용
- @AfterAll = 종료하기전에 한번만 실행, db연결 종료, 공통적으로 사용하는 자원을 해제할때 사용
- @AfterEach = 테스트 이후에 특정 데이터 삭제
🔻결과
4.📌AssertJ로 검증문 가독성 높이기
AssertJ는 JUnit과 함께 검증문의 가독성을 높여주는 라이브러리이다
🔻기댓값과 비교값이 잘 구분되지 않는 Assertion
Assertions.assertEquals(sum, a+b);
🔻가독성이 좋은 AssertJ
assertThat(a+b).isEqualTo(sum)
a+b를 더한 값이 sum과 같아야한다라는 의미로 명확해짐
이외에도 사용하는 다양한 메서드
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보다 작은값인지 검증 |
5.📌테스트 코드작성 연습문제 풀어보기
🔻Q1. string으로 선언한 변수 3개가 있다 여기에서 세 변수 모두 null이 아니며 name1과 name2는 같은 값을 가지며 name3은 다른 나머지 두 변수와 다른 값을 가진다 이를 검증하는 테스트를 작성해봐라
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class JUnitQuiz {
@Test
public void junitTest(){
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);
}
}
🔻Q2. int로 선언된 변수 3개가 있다 number1, number2, number3는 각각 15, 0 , -1의 값을 가집니다 세변수가 각각 양수, 0 ,음수이고 number1은 number2보다 큰 값이고, number 3는 number2보다 작은 값임을 검증하는 테스트를 작성해라
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class JUnitQuiz {
@Test
public void junitTest2(){
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);
}
}
🔻Q3. JUnitCycleQuiz.java 파일을 추가하고 각각의 테스트를 시작하기 전에 "Hello!"를 출력하는 메서드와 모든 테스트를 끝마치고 "Bye!"를 출력하는 메서드를 추가해라
import org.junit.jupiter.api.Test;
public class JUnitCycleQuiz {
@Test
public void junitQuiz3(){
System.out.println("This is first test");
}
@Test
public void junitQuiz4(){
System.out.println("This is second test");
}
}
다음과 같은 클래스가 있을때 실행하면
Hello!
This is first test
Hello!
This is second test
Bye!
위와 같이 출력되려면 어떻게 해야할까
→
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class JUnitCycleQuiz {
@BeforeEach
public void beforeEach(){
System.out.println("Hello!");
}
@AfterAll
public static void afterAll(){
System.out.println("Bye!");
}
@Test
public void junitQuiz3(){
System.out.println("This is first test");
}
@Test
public void junitQuiz4(){
System.out.println("This is second test");
}
}
6.📌 제대로 테스트 코드 작성해보기
TestController.java 파일에서 클래스 이름위에 커서를 두고 Alt + enter누르면 create test가 생긴다
여기서 ok누르면 test파일 생성
@SpringBootTest // 테스트를 위한 어플리케이션 컨텍스트를 제공
@AutoConfigureMockMvc // MockMvc를 자동 설정
class TestControllerTest {
@Autowired
protected MockMvc mockMvc; // 웹 API 테스트를 위한 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를 생성하고 자동으로 구성하는 애너테이션. 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스
로직 테스트하는 코드 작성(TestControllerTest.java)
...
@DisplayName("getAllMembers: 아티클 조회에 성공한다")
@Test
public void getAllMembers() throws Exception {
final String url = "/test";
Member saveMember = memberRepository.save(new Member(1L, "홍길동"));
final ResultActions result = mockMvc.perform(get(url).accept(MediaType.APPLICATION_JSON));
result.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(saveMember.getId()))
.andExpect(jsonPath("$[0].name").value(saveMember.getName()));
}
- perform() 메서드는 요청을 전송하는 역할을 하는 메서드, 결과로 ResultActions 객체를 받으며 ResultActions는 반환값을 검증하고 확인하는 andExpect()메서드를 제공한다
- accept() 메서드는 요청을 보낼 때 무슨 타입으로 응답 받을지 결정하는 메서드
- andExpect() 메서드는 응답을 검증한다 TestController에서 만든 API는 응답으로 ok(200)을 반환해 응답코드가 ok(200)인지 확인
- jsonPath("$[0].$필드명")은 json응답값을 가져오는 역할을 하는 메서드이다 0번째 배열에 들어있는 객체의 id.name 값을 가져오고 저장된 값과 같은지 확인
🔻HTTP 주요 응답 코드
200 ok | isOk() |
201 Created | isCreated() |
400 Bad Request | isBadRequest() |
403 Forbidden | isForbidden() |
404 Not Found | isNotFound() |
400번대 응답코드 | is4xxClientError() |
500 Internal Server Error | isInternalServerError() |
500번대 응답코드 | is5xxServerError() |
7.📌테스트 코드 패턴 연습하기
me.shinsunyoung.springbootdeveloper폴더 안에 QuizController.java만들기
public class QuizController {
@GetMapping("/quiz")
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("OK");
}
}
@PostMapping("/quiz")
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) {
}
/quiz로 GET요청이 오면 quiz()라는 메서드에서 요청을 처리한다
요청 파라미터의 키가 "code"면 int 자료형의 code 변수와 매핑되어 값에 따라 다른 응답을 보낸다
code 값 | 응답코드 | 응답본문 |
1 | 201 | Created! |
2 | 400 | Bad Request! |
그외 | 200 | OK! |
quiz로 POST요청이 오면 quiz2라는 메서드에서 요청을 처리한다
code값 | 응답코드 | 응답 본문 |
1 | 403 | Forbidden! |
그외 | 200 | OK! |
🔻테스트 코드 만들기
@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간의 변환을 처리해준다
Code code = new Code(13)
objectMapper.writeValueAsString(code)
이렇게 code객체를 만들고 메서드를 호출하면 {'value' : 13}인 JSON 형태의 문자열로 객체가 변환된다
이를 객체 직렬화라고 말한다
8.📌테스트 코드 연습문제 풀어보기
Q1 .GET 요청을 보내 응답 코드마다 예상하는 응답을 반환하는지 검증하는 테스트를 작성해라
@DisplayName("quiz(): GET /quiz?code=1이면 응답코드는 201, 응답 본문은 Created!를 리턴한다.")
@Test
public void quiz() 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 quiz2() 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"));
}
Q2. POST 요청을 보내 응답 코드마다 예상하는 응답을 반환하는지 검증하는지 테스트를 작성해라
@DisplayName("quiz(): POST /quiz?code=1이면 응답코드는 403, 응답본문은 Forbidden!을 리턴한다")
@Test
public void postQuiz1() throws Exception {
final String url = "/quiz";
final ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new code(1))));
result.andExpect(status().isForbidden())
.andExpect(content().string("Forbidden!"));
}
@DisplayName("quiz(): POST /quiz?code=13이면 응답코드는 200, 응답본문은 OK를 리턴한다")
@Test
public void postQuiz13() throws Exception {
final String url = "/quiz";
final ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new code(13))));
result.andExpect(status().isOk())
.andExpect(content().string("OK"));
}
'spring > spring 이론' 카테고리의 다른 글
데이터베이스 조작이 편해지는 ORM (0) | 2024.12.24 |
---|---|
스프링 부트 3 코드&구조 이해하기 (0) | 2024.12.21 |
스프링 부트 3 둘러보기 (1) | 2024.12.20 |
스프링 부트 시작하기 (0) | 2024.12.20 |
백엔드 개발자가 알아두면 좋은 지식 (1) | 2024.12.19 |