[Spring Boot] 4. 스프링 부트3와 테스트

김민경·2024년 7월 2일
post-thumbnail

'스프링 부트3 백엔드 개발자 되기' 책을 참고하며 작성 중 입니다.


테스트 코드?

작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로 작성하는 코드
유지보수에 좋고, 코드 수정 시 기존 기능이 제대로 작동하지 않을까봐 걱정하지 않아도 된다!

테스트 코드 패턴

given - when - then

given : 테스트 실행을 준비하는 단계
when : 테스트를 진행하는 단계
then : 테스트 결과를 검증하는 단계


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

JUnit

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

Spring Test & Spring Boot Test

스프링 부트 애플리케이션을 위한 통합 테스트 지원

AssertJ

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

Hamcrest

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

Mockito

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

JSONassert

JSON용 어설션 라이브러리

JsonPath

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


JUnit

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

단위 테스트?

작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것

특징

  1. 테스트 방식을 구분할 수 있는 애너테이션 제공
  2. @Test 애너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능
  3. 예상 결과를 검증하는 어설션 메서드 제공
  4. 사용 방법이 단순, 테스트 코드 작성 시간이 적음
  5. 자동 실행, 자체 결과를 확인하고 즉각적인 피드백 제공

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

JUnitTest

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 : 테스트를 수행하는 메서드

테스트를 위한 실행 객체를 만들고 종료 후 삭제하여 각 테스트끼리 영향을 주지 않도록 한다.

JUnitCycleTest

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 실행 결과


AssertJ

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

  • 기댓값과 비교값이 잘 구분되지 않는 Assertion 예

    	Assertions.assertEquals(sum, a+b);
  • 가독성이 좋은 AssertJ 예

    	assertThat(a+b).isEqualTo(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보다 작은 값인지

테스트 코드 작성 연습

JunitTest

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

JUnitCycleQuiz

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!");
   }
}

결과


실제 테스트 코드 작성

TestController

@RestController
public class TestController {
  @Autowired
  TestService testService;

  @GetMapping("/test")
  public List<Member> getAllMembers(){
      List<Member> members = testService.getAllMembers();
      return members;
  }
}

TestControllerTest

//..생략
@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 OKisOk()
201 CreatedisCreated()
400 Bad RequestisBadRequest()
403 ForbiddenisForbidden()
404 NotFoundisNotFound()
400번대 응답 코드is4xxClientError()
500 Internal Server ErrorisInternalServerError()
500번대 응답 코드is5xxServerError()

테스트 코드 패턴 연습

QuizController

@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() 메서드 등을 자동으로 생성

QuizControllerTest

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

}
profile
뭐든 기록할 수 있도록

0개의 댓글