스프링 부트3 - 테스트 코드

김동헌·2023년 11월 9일
0

SpringBoot

목록 보기
5/19
post-thumbnail

1. 테스트 코드

우테코를 진행하면서 테스트 코드를 처음으로 사용해 보았습니다.
테스트 코드의 단점으로 개발 속도 지연이라는 부분을 찾을 수 있었는데 실제로 해보니 처음에는 어느정도 공감이 되었으나 테스트 코드가 구현되고 난 뒤 실제 구현을 할 때, 뭔가 재미있었고 오류가 크게 나오지 않았습니다. 또한 수정 과정 속에서 디버깅도 훨씬 편리했습니다.

실제 구현을 위한 개발 공부도 좋지만 테스트 코드에 관심을 한번 가지고 실제로 해보시는 것도 좋은 경험이 될 것 같습니다.


1.1 테스트 코드 ?

테스트 코드에는 다양한 패턴이 있습니다. 이중 제가 공부한 패턴은 given-when-then 패턴입니다. 이 패턴은 테스트 코드를 세 단계로 구분해 작성하는 방식입니다.

1. given은 테스트 실행을 준비하는 단계
2. when은 테스트를 진행하는 단계
3. then은 테스트 결과를 검증하는 단계

🔽 given-when-then

public class example {
    @DisplayName("새로운 메뉴를 저장한다.")
    @Test
    public void saveMenuTest() {
        // given → 테스트 준비
        final String name = "사과";
        final int price = 4000;

        // when → 테스트 검증을 위한 준비
        final Menu apple = new Menu(name, price);
        
        // then → 테스트 결과 검증
        final long savedId = menuService.findById(savedId).get();
        assertThat(saveMenu.getName().isEqualTo(name));
        assertThat(saveMenu.getPrice().isEqualTo(price));
    }
}

2. 스프링 부트 스타터 테스트

스프링 부트는 애플리케이션 테스트를 위한 도구와 애너테이션을 제공합니다.

🔽 스프링 부트 스타터 테스트 종류

종류설명
JUnit자바 프로그래밍 언어용 단위 테스트 프레임워크
Spring Test & SpringBoot Test스프링 부트 애플리케이션을 위한 통합 테스트 지원
AssertJ검증문인 어설션을 제공하는 라이브러리
Hamcrest표현식을 보다 이해하기 쉽게 만드는데 필요한 Matcher 라이브러리
Mockito테스트에 사용할 가짜객체(Mock)를 쉽게 생성, 관리, 검증 지원하는 테스트 프레임워크
JSonassertJSON용 어설션 라이브러리
JsonPathJson 데이터에서 특정 데이터를 선택, 검색하기 위한 라이브러리

이중 JUnitAssertJ를 가장 많이 사용하고, Mockito는 제가 생각할 때 유용하고 편리해서 따로 체크해두었습니다.


2.1 JUnit

개발을 할 때 좋은 코드는 클래스(객체)를 분리하고, 함수(또는 메서드)가 한 가지 일만 하도록 최대 작게 만들어야 한다고 합니다.
함수(또는 메서드)는 단위로 볼 수 있고, 이러한 작은 단위를 검증할 때, 유용한 것이 JUnit입니다.

JUnit 특징
1) 테스트 방식을 구분할 수 있는 애너테이션 제공

2) @Test 애너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능 → 테스트 종료 후 실행 객체 삭제

3) 예상 결과를 검증하는 어설션 메서드 제공

4) 사용 방법이 단순, 테스트 코드 작성 시간이 적음

5) 자동 실행, 자체 결과를 확인하고 즉각적인 피드백 제공


2.1.1 테스트 케이스 - 한개


import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class example {
    @DisplayName("7 + 7 = 14 입니다.") // 테스트 이름 작성
    @Test // 테스트 메서드
    public void JUnitTest() {
        int a = 7;
        int b = 7;
        int sum = 14;

        Assertions.assertEquals(a+b, sum); // 동일한 값인지 확인
    }
}

Assertions.assertEquals();은 설명하지 않았으나 어떤 느낌인지 아시겠나요 ?
메서드들의 기능을 보려면 Ctrl을 누른 상태로 해당 메서드를 눌러보면 된다고 했었습니다. 한번 들어가볼게요 : >
assertEquals 메서드의 코드입니다.

	Assert that expected and actual are equal.
	public static void assertEquals(int expected, int actual) {
		AssertEquals.assertEquals(expected, actual);
	}

첫 번째 인수는 기댓값(expected),
두 번째 인수에는 실젯값(actual)을 넣어서 동일한 값인지 확인하는 메서드라고 되어 있습니다.

실제로 실행을 해봅시다 : >

@DisplaName이 왼쪽 하단에 보이면서 테스트가 정상적으로 동작했다고 나오네요. 만약 실패한다면 어떻게 될까요 ? 직접 해봅시다 !

        int a = 7;
        int b = 6;
        int sum = 14;

        Assertions.assertEquals(a+b, sum); // 동일한 값인지 확인

실패하면 기댓값과 실젯값이 다르다고 나오면서 저희가 두려워하는 빨간색상 폰트로 상세히 알려줍니다. 그러면서 public class JUnitTest { } 테스트 실패라고 아이콘이 뜨게 됩니다.
즉, JUnit는 단위 테스트 케이스 실패 → 전체 테스트 실패를 의미합니다.


2.1.2 테스트 케이스 - 여러개

❗ 이제 테스트를 여러개 해봅시다 !

import org.junit.jupiter.api.*;

public class JUnitTest {

    @BeforeAll // 전체 테스트 케이스 시작하기 전에 1회 실행됨, 메서드는 static으로 선언
    static void beforeAll() {
        System.out.println("@BeforeAll - 전체 테스트 케이스 시작하기 전에 1회 실행됨");
    }

    @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 - 전체 테스트 케이스 종료되기 전에 1회 실행");
    }

    @AfterEach // 테스트 케이스 종료하기 전마다 실행
    public void afterEach(){
        System.out.println("@AfterEach - 테스트 케이스 종료되기 전마다 실행");
    }

}

🔽실행 결과

관련해서 정리해보았습니다 : >


2.2 AssertJ - assertThat()

AssertJ는 JUnit과 함께 사용해 검증문의 가독성을 확 높여주는 라이브러리입니다. 앞에서 작성한 테스트 코드의
Assertions는 기댓값과 실제 비교값을 명시하지 않으므로 비교 대상이 잘 구분되지 않았습니다.
그래서 Ctrl을 하고 들어갔었죠 ? 이러한 부분은 가독성이 좋지 않다는 것을 의미합니다. 프로젝트의 규모가 점차 증대함에 따라 가독성은 꽤 중요한 문제입니다.

AssertJ는 검증문인 어설션을 제공한다고 했습니다. 코드로 보겠습니다 :>

🔽 Assertion
Assertions.assertEquals(a+b, sum);

🔽 AssertJ
assertThat(a+b).isEqualTo(sum)

❗ 둘 다 모두 기댓값 a+b는 실젯값 sum과 같아야 한다는 검증문이지만 Assertion은 어떤 것이 기댓값이고, 어떤 것이 실젯값인지 알아보기 어렵지만,
AssertJ는 확실히 가독성이 뛰어나는 것을 확인할 수 있네요 !


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보다 작은 값인지 검증

2.3 스프링부트 - TestCode 작성

스프링 부트3 - 구조 → 프레젠테이션 계층 코드(TestController.java code 참고

TestController.java 파일에서 클래스 이름에서 Alt+Enter를 누르면 Test를 만들 수 있습니다.


2.3.1 TestControllerTest 세팅

🔽 TestControllerTest

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;

@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();
    }
    
    @AfterEach
    public void cleanUp() {
        memberRepository.deleteAll();
    }
}

🔍 @AutoConfigureMockMvc : MockMvc를 생성하고 자동으로 구성하는 애너테이션, MockMvc는 어플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청과 전송, 응답 기능을 제공하는 유틸리티 클래스로 컨트롤러를 테스트할 때 사용되는 클래스

🔍 @BeforeEach : 테스트를 실행하기 전에 실행하는 메서드에 적용하는 애너테이션으로, MockMvcSetUp( ) 메서드를 실행해 MockMvc를 설정

🔍 @AfterEach : 테스트를 실행한 이후에 실행하는 메서드에 적용하는 애너테이션으로, clenUp() 메서드를 실행해 member 테이블에 있는 모든 데이터들을 삭제한다.


2.3.2 TestControllerTest 구현

TestControllerTest의 로직을 테스트하는 코드를 구현해보겠습니다 : >

🔽 TestControllerTest

    @DisplayName("getAllMembers: 아티클 조회에 성공")
    @Test
    public void getAllMembers() throws Exception {

        // given
        final String url = "/test";
        Member savedMember = memberRepository.save(new Member(1L, "김동헌"));

        // when
        final ResultActions result = mockMvc.perform(get(url) // (1)
                .accept(MediaType.APPLICATION_JSON)); // (2)

        // then
        result
                .andExpect(status().isOk()) // (3)
                // (4) 응답의 0번째 값이 DB의 값과 동일한지 검증
                .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
                .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
    }
}
    

한번 살펴보겠습니다.

먼저 주석처리가 되어있는 given, when, then이 기억나시나요 ?

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

TestControllerTest에서의 given-when-then패턴은 어떤 역할을 할까요 ?

TestControllerTest의 given
→ 멤버를 저장

TestControllerTest의 when
→ 멤버 리스트를 조회하는 API를 호출

TestControllerTest의 then
→ 응답 코드가 200 OK이며, 반환 받은 값 중에 0번째 요소의 id와 name이 저장된 값과 동일한지 확인


어떤 느낌인지 이해는 되셨나요 ?
이번엔 TestControllerTest코드를 살펴보겠습니다 !


(1) perform() : 요청을 전송하는 역할로 결과를 ResultActions 객체를 통해서 받으며, ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드를 제공합니다. 추가 설명 → (3)

final ResultActions result = mockMvc.perform(get(url) // (1)
.accept(MediaType.APPLICATION_JSON)); // (2)

🔽 andExpect() 관련 구성 코드

/**
 * Perform a request and return a type that allows chaining further
 * actions, such as asserting expectations, on the result.
 * @param requestBuilder used to prepare the request to execute;
 * see static factory methods in
 * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
 * @return an instance of {@link ResultActions} (never {@code null})
 * @see org.springframework.test.web.servlet.request.MockMvcRequestBuilders
 * @see org.springframework.test.web.servlet.result.MockMvcResultMatchers
 */
public ResultActions perform(RequestBuilder requestBuilder) throws Exception {
	if (this.defaultRequestBuilder != null && requestBuilder instanceof Mergeable mergeable) {
		requestBuilder = (RequestBuilder) mergeable.merge(this.defaultRequestBuilder);
	}
}
        


(2) accept() : 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드입니다. 함수(또는 메서드) 정의을 할 때 함수(또는 메서드)의 반환값 자료형 선언과 같은 느낌이네요 !
여기서는 JSON, XML 등 다양한 타입이 있지만 JSON을 받는다고 명시했습니다.

final ResultActions result = mockMvc.perform(get(url) // (1)
.accept(MediaType.APPLICATION_JSON)); // (2)

🔽 accept() 관련 구성 코드

/**
 * Set the 'Accept' header to the given media type(s).
 * @param mediaTypes one or more media types
 */
public MockHttpServletRequestBuilder accept(MediaType... mediaTypes) {
	Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
	this.headers.set("Accept", MediaType.toString(Arrays.asList(mediaTypes)));
	return this;
}


(3) andExpect() : 응답을 검증합니다. TestController에서 만든 API는 응답으로 OK(200)을 반환하기 때문에, 이에 해당하는 메서드인 isOk를 사용해 응답 코드가 Ok(200)인지 확인 합니다.

result
  .andExpect(status().isOk()) // (3)
  // (4) 응답의 0번째 값이 DB의 값과 동일한지 검증
       .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
       .andExpect(jsonPath("$[0].name").value(savedMember.getName()));

🔽 andExpect() 관련 구성 코드

ResultActions andExpect(ResultMatcher matcher) throws Exception;

(4) jsonPath("$[0].id") : JSON 응답값의 값을 가져오는 역할입니다. 0번째 배열에 들어있는 객체의 id, name 값을 가져오고, 저장된 값과 같은지 확인합니다.



2.3.3 HTTP 응답 코드

HTTP 주용 응답 코드는 알아두면 좋습니다.
참고하기 편하게 표로 작성하였습니다 : >

🔽 HTTP 상태 코드

코드응답
200 OK요청이 성공적으로 완료
201 created요청이 성공적이었으며, 그 결과로 새로운 리소스 생성
이 응답은 일반적으로 POST 요청 또는 일부 PUT 요청 이후에 따라옵니다.
400 Bad Request이 응답은 잘못된 문법으로 인하여 서버가 요청을 이해할 수 없음을 의미합니다.
403 Forbidden클라이언트는 콘텐츠에 접근할 권리를 가지고 있지 않습니다. 예를들어 그들은 미승인이어서 서버는 거절을 위한 적절한 응답을 보냅니다. 401과 다른 점은 서버가 클라이언트가 누구인지 알고 있습니다.
404 Not Found서버는 요청받은 리소스를 찾을 수 없습니다. 브라우저에서는 알려지지 않은 URL을 의미합니다. API에서 종점은 적절하지만 리소스 자체는 존재하지 않음을 의미할 수도 있습니다.
서버들은 인증받지 않은 클라이언트로부터 리소스를 숨기기 위하여 이 응답을 403 대신에 전송할 수도 있습니다.
400 번대 응답 코드클라이언트 에러 응답
500 Internal Server Error서버 에러 응답
500 번대 응답 코드서버가 처리 방법을 모르는 상황이 발생했습니다. 서버는 아직 처리 방법을 알 수 없습니다.

참고 링크 - mozilla

🔽 HTTP 응답 코드

코드매핑 메서드설명
200 OKisOk()HTTP 응답 코드가 200 OK인지 검증
201 createdisCreatedHTTP 응답 코드가 201 Created인지 검증
400 Bad RequestisBadRequestHTTP 응답 코드가 400 Bad Request인지 검증
403 ForbiddenisForbidden()HTTP 응답 코드가 403 Forbidden인지 검증
404 Not FoundisNotFound()HTTP 응답 코드가 404 Not Found인지 검증
400 번대 응답 코드is4xxClientError()HTTP 응답 코드가 400번대 응답 코드인지 검증
500 Internal Server ErrorisInternalServerError()HTTP 응답 코드가 500 Internal Server Error인지 검증
500 번대 응답 코드is5xxServerError()HTTP 응답 코드가 500 OK인지 검증

profile
백엔드 기록 공간😁

0개의 댓글