
'스프링 부트3 백엔드 개발자 되기' 책을 참고하며 작성 중 입니다.
작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로 작성하는 코드
유지보수에 좋고, 코드 수정 시 기존 기능이 제대로 작동하지 않을까봐 걱정하지 않아도 된다!
given : 테스트 실행을 준비하는 단계
when : 테스트를 진행하는 단계
then : 테스트 결과를 검증하는 단계
JUnit
자바 프로그래밍 언어용 단위 테스트 프레임워크
Spring Test & Spring Boot Test
스프링 부트 애플리케이션을 위한 통합 테스트 지원
AssertJ
검증문인 어설션을 작성하는 데 사용되는 라이브러리
Hamcrest
표현식을 이해하기 쉽게 만드는 데 사용되는 Matcher 라이브러리
Mockito
테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크
JSONassert
JSON용 어설션 라이브러리
JsonPath
JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리
자바 언어를 위한 단위 테스트 프레임워크
작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것
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);
assertThat(a+b).isEqualTo(sum);//a+b를 더한 값이 sum과 같아야 한다.
}
}
@DisplayName : 테스트 이름 명시
@Test : 테스트를 수행하는 메서드
테스트를 위한 실행 객체를 만들고 종료 후 삭제하여 각 테스트끼리 영향을 주지 않도록 한다.
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 : 전체 테스트를 시작하기 전 처음으로 한 번만 실행, static 선언
@BeforeEach : 각 테스트 케이스를 시작하기 전 매번 실행
@AfterAll : 전체 테스트를 마치고 종료하기 전 한 번만 실행, static 선언
@AfterEach : 각 테스트 케이스를 종료하기 전 매번 실행
JUnitCycleTest 실행 결과
JUnit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리
기댓값과 비교값이 잘 구분되지 않는 Assertion 예
Assertions.assertEquals(sum, a+b);
가독성이 좋은 AssertJ 예
assertThat(a+b).isEqualTo(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보다 작은 값인지 |
public class JUnitQuiz {
@Test
public void junitTest(){
String name1 = "홍길동";
String name2 = "홍길동";
String name3 = "홍길은";
assertThat(name1).isNotNull();
assertThat(name2).isNotNull();
assertThat(name3).isNotNull();
assertThat(name1).isEqualTo(name2);
assertThat(name2).isNotEqualTo(name3);
}
@Test
public void junitTest2(){
int number1 = 15;
int number2 = 0;
int number3 = -5;
assertThat(number1).isPositive();
assertThat(number2).isZero();
assertThat(number3).isNotPositive();
assertThat(number1).isGreaterThan(number2);
assertThat(number3).isLessThan(number2);
}
}
import org.junit.jupiter.api.*;
public class JUnitCycleQuiz {
@BeforeEach
public void beforeEach(){
System.out.println("Hello!");
}
@Test
public void junitQuiz3(){
System.out.println("This is first test");
}
@Test
public void junitQuiz4(){
System.out.println("This is second test");
}
@AfterAll
static void afterAll(){
System.out.println("Bye!");
}
}
결과
@RestController
public class TestController {
@Autowired
TestService testService;
@GetMapping("/test")
public List<Member> getAllMembers(){
List<Member> members = testService.getAllMembers();
return members;
}
}
//..생략
@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();
//mockMvc 설정
}
@AfterEach
public void cleanUp(){
memberRepository.deleteAll();
//member 테이블에 있는 데이터들을 모두 삭제
}
@DisplayName("getAllMembers : 아티클 조회에 성공한다")
@Test
public void getAllMembers() throws Exception {
//given
//멤버 저장
final String url = "/test";
Member savedMember = memberRepository.save(new Member(1L, "홍길동"));
//when
//멤버 리스트 조회하는 API 호출
//ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드 제공
final ResultActions result = mockMvc.perform(get(url).
//perform() -> 요청을 전송하는 역할
accept(MediaType.APPLICATION_JSON));
//accept()는 무슨 타입으로 응답을 받을지 결정하는 메서드
//then
//응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인
result
//andExpect()는 응답을 검증
.andExpect(status().isOk()) //응답 코드가 OK(200)인지 확인지
.andExpect(jsonPath("$[0].id").value(savedMember.getId())) //JSON 응답값을 가져오는 역할
.andExpect(jsonPath("$[0].name").value(savedMember.getName()));
}
}
@SpringBootTest : @SpringBootApplication이 있는 클래스를 찾고 그 클래스에 포함되어 있는 빈을 찾은 다음 테스트용 애플리케이션 컨텍스트라는 것을 만든다.
@AutoConfigureMockMvc : MockMvc 생성 및 자동 구성
MockMvc? : 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스
컨트롤러를 테스트할 때 사용되는 클래스
given
Member 객체 저장
when
1. perform() : 요청을 전송하는 역할
2. ResultActions : 반환값을 검증하고 확인하는 andExpect() 메서드 제공
3. accept() : 무슨 타입으로 응답을 받을지 결정하는 메서드
then
1. andExpect() : 응답 검증
2. jsonPath : JSON 응답값의 값을 가져오는 역할
HTTP 주요 응답 코드
코드 매핑 메서드 200 OK isOk() 201 Created isCreated() 400 Bad Request isBadRequest() 403 Forbidden isForbidden() 404 NotFound isNotFound() 400번대 응답 코드 is4xxClientError() 500 Internal Server Error isInternalServerError() 500번대 응답 코드 is5xxServerError()
@RestController
public class QuizController {
@GetMapping("/quiz")
public ResponseEntity<String> quiz(@RequestParam("code") int code){
return switch (code) {
case 1 -> ResponseEntity.created(null).body("Created!");
case 2 -> ResponseEntity.badRequest().body("Bad Request!");
default -> ResponseEntity.ok().body("OK!");
};
}
@PostMapping("/quiz")
public ResponseEntity<String> quiz2(@RequestBody Code code){
if (code.value() == 1) {
return ResponseEntity.status(403).body("Forbidden!");
}
return ResponseEntity.ok().body("OK!");
}
}
record Code(int value) {};
//record를 사용하면 필드, 생성자, 게터, equals(), hashCode(), toString() 메서드 등을 자동으로 생성
@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();
}
@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, 응답 본문은 BadRequest!를 리턴")
@Test
public void getQuiz2() throws Exception{
final String url = "/quiz";
final ResultActions result = mockMvc.perform(get(url).param("code", "2"));
result.andExpect(status().isBadRequest())
.andExpect(content().string("Bad Request!"));
}
@DisplayName("quiz(): POST /quiz?code=1 이면 응답 코드는 403, 응답 본문은 Forbidden!를 리턴")
@Test
public void getQuiz3() throws Exception{
final String url = "/quiz";
final ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Code(1))));
//JSON형식으로 전환
result.andExpect(status().isForbidden())
.andExpect(content().string("Forbidden!"));
}
@DisplayName("quiz(): POST /quiz?code=13 이면 응답 코드 200, 응답 본문 OK! 리턴")
@Test
public void getQuiz4() 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!"));
}
}