leehyeon-dv 님의 블로그

스프링 부트와 테스트 본문

spring/spring 이론

스프링 부트와 테스트

leehyeon-dv 2024. 12. 21. 23:18

✨목차

  1. 테스트 코드란?
  2. 스프링 부트3와 테스트
  3. JUnit을 실제로 사용해보기
  4. AssertJ로 검증문 가독성 높이기
  5. 테스트 코드작성 연습문제 풀어보기
  6. 제대로 테스트 코드 작성해보기
  7. 테스트 코드 패턴 연습하기
  8. 테스트 코드 연습문제 풀어보기

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