
우테코를 진행하면서 테스트 코드를 처음으로 사용해 보았습니다.
테스트 코드의 단점으로개발 속도 지연이라는 부분을 찾을 수 있었는데 실제로 해보니 처음에는 어느정도 공감이 되었으나 테스트 코드가 구현되고 난 뒤 실제 구현을 할 때, 뭔가 재미있었고 오류가 크게 나오지 않았습니다. 또한 수정 과정 속에서 디버깅도 훨씬 편리했습니다.실제 구현을 위한 개발 공부도 좋지만 테스트 코드에 관심을 한번 가지고 실제로 해보시는 것도 좋은 경험이 될 것 같습니다.
테스트 코드에는 다양한 패턴이 있습니다. 이중 제가 공부한 패턴은 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));
    }
}
스프링 부트는 애플리케이션 테스트를 위한 도구와 애너테이션을 제공합니다.
🔽 스프링 부트 스타터 테스트 종류
| 종류 | 설명 | 
|---|---|
JUnit | 자바 프로그래밍 언어용 단위 테스트 프레임워크 | 
| Spring Test & SpringBoot Test | 스프링 부트 애플리케이션을 위한 통합 테스트 지원 | 
AssertJ | 검증문인 어설션을 제공하는 라이브러리 | 
| Hamcrest | 표현식을 보다 이해하기 쉽게 만드는데 필요한 Matcher 라이브러리 | 
Mockito | 테스트에 사용할 가짜객체(Mock)를 쉽게 생성, 관리, 검증 지원하는 테스트 프레임워크 | 
| JSonassert | JSON용 어설션 라이브러리 | 
| JsonPath | Json 데이터에서 특정 데이터를 선택, 검색하기 위한 라이브러리 | 
이중
JUnit과AssertJ를 가장 많이 사용하고,Mockito는 제가 생각할 때 유용하고 편리해서 따로 체크해두었습니다.
개발을 할 때 좋은 코드는 클래스(객체)를 분리하고, 함수(또는 메서드)가 한 가지 일만 하도록 최대 작게 만들어야 한다고 합니다.
함수(또는 메서드)는 단위로 볼 수 있고, 이러한 작은 단위를 검증할 때, 유용한 것이 JUnit입니다.
 JUnit 특징 
1) 테스트 방식을 구분할 수 있는 애너테이션 제공 
2) @Test 애너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능 → 테스트 종료 후 실행 객체 삭제
3) 예상 결과를 검증하는 어설션 메서드 제공
4) 사용 방법이 단순, 테스트 코드 작성 시간이 적음
5) 자동 실행, 자체 결과를 확인하고 즉각적인 피드백 제공
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는 단위 테스트 케이스 실패 → 전체 테스트 실패를 의미합니다.
❗ 이제 테스트를 여러개 해봅시다 !
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 - 테스트 케이스 종료되기 전마다 실행");
    }
}
🔽실행 결과
관련해서 정리해보았습니다 : >
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보다 작은 값인지 검증 | 
스프링 부트3 - 구조 → 프레젠테이션 계층 코드(TestController.java code 참고
TestController.java파일에서 클래스 이름에서Alt+Enter를 누르면 Test를 만들 수 있습니다.
🔽 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 테이블에 있는 모든 데이터들을 삭제한다.
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이 기억나시나요 ?
given : 테스트 실행을 준비하는 단계when : 테스트를 진행하는 단계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 값을 가져오고, 저장된 값과 같은지 확인합니다.
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 번대 응답 코드 | 서버가 처리 방법을 모르는 상황이 발생했습니다. 서버는 아직 처리 방법을 알 수 없습니다. | 
🔽 HTTP 응답 코드 
| 코드 | 매핑 메서드 | 설명 | 
|---|---|---|
| 200 OK | isOk() | HTTP 응답 코드가 200 OK인지 검증 | 
| 201 created | isCreated | HTTP 응답 코드가 201 Created인지 검증 | 
| 400 Bad Request | isBadRequest | HTTP 응답 코드가 400 Bad Request인지 검증 | 
| 403 Forbidden | isForbidden() | HTTP 응답 코드가 403 Forbidden인지 검증 | 
| 404 Not Found | isNotFound() | HTTP 응답 코드가 404 Not Found인지 검증 | 
| 400 번대 응답 코드 | is4xxClientError() | HTTP 응답 코드가 400번대 응답 코드인지 검증 | 
| 500 Internal Server Error | isInternalServerError() | HTTP 응답 코드가 500 Internal Server Error인지 검증 | 
| 500 번대 응답 코드 | is5xxServerError() | HTTP 응답 코드가 500 OK인지 검증 | 
