📘 Backend/Spring

Testing (Mockito) & Assertion

신건우 2023. 4. 3. 08:09

📘 Test

JUnit은 표준 테스트 프레임워크이다

img

  • 기능 테스트
    • 테스트의 범위 중 제일 큰 테스트, 어플리케이션 전체에 걸친 테스트
  • 통합 테스트
    • 테스트 주체가 어플리케이션 제작 개발팀 or 개발자 단위 테스트, 클라이언트 툴 없음
  • 슬라이스 테스트
    • 어플리케이션을 특정 계층으로 나눠서 테스트
  • 단위 테스트
    • 어플리케이션의 핵심 비즈니스 로직 메소드의 독립적인 테스트

단위 테스트의 F.I.R.S.T 원칙

  • Fast
  • Independent
  • Repeatable
  • Self-validating
  • Timely

given - when - then Pattern

  • given
    • 테스트에 필요한 전제조건 포함
  • when
    • 테스트 동작 지정
  • then
    • 테스트 결과 검증, 값 비교 (Assertion)

Hamcrest를 사용한 Assertion

  • Assertion을 위한 Matcher를 자연스러운 문장으로 이어지도록 하며, 가독성 향상
  • 테스트 실패 시 손쉬운 원인 파악 가능, 다양한 Matcher 제공

Controller Test

  • MockMvc, Gson @Autowired 주입
  • 테스트 대상 핸들러 메소드 호출 throws Exception
  • Dto 호출, 객체 생성, 데이터 삽입
  • gson.toJson() 데이터 변환
  • ResultAction 타입의 mockMvc.preform을 이용한 컨트롤러 정보 입력
    post(), accept(), contentType(), content()를 이용해 requestbody 설정
  • MvcResult 타입의 actions.andExcept("$.data.멤버"), .andRetuen()을 이용해 데이터 검증

Data Test

  • @DataJpaTest
  • Repo Autowired
  • Entity 객체 생성 -> 데이터 set
  • save
  • Assertion

📘 구현


Packages

  • import static org.assertj.core.api.Assertions.*; [assertj]
  • import static org.junit.jupiter.api.Assertions.*; [junit]
  • import static org.junit.jupiter.api.Assumptions.*; [Assumptions]
  • import static org.hamcrest.MatcherAssert.*; [Hamcrest]
  • import static org.hamcrest.Matchers.*; [Hamcrest Matcher]
  • import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; [Controller 테스트]
  • import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; [Controller 테스트]
  • import org.mockito.Mockito; [Mockito]
  • import static org.mockito.BDDMockito.given; [Mockito_given]

Class, Method

  • Junit
    • assertEquals(a,b) - 값비교
    • assertNotNull(대상, 실패시 메시지) - Null 여부 체크
    • assertThrows(Exception.class, () -> 테스트대상 메소드) - 예외 발생 테스트*assertDoesNotThrow(() -> Do) - *예외 발생 X 테스트

  • AsserJ

  • Assumption
    • assumeTrue() - 파라미터의 값이 true이면, 아래 로직 실행

  • Hamcrest
    • asserThat(a, is(equalTo(b))) - 비교
    • asserThat(a, is(notNullValue())) - Null 검증
    • asserThat(대상.class, is(예상Exception.class)) - 예외 검증

  • URI
    • UriComponentBuilder.newInstance().path().buildAndExpand().toUri - Build Request URI

  • ResultActions - 기대 HTTP Status, Content 검증
    • mockMvc.perform(get & post 등등)

  • MvcResult - Response Body의 HTTP Status, 프로퍼티 검증
    • ResultActions의 객체를 이용

Annotations

@BeforeEach - init() 사용, 테스트 실행 전 전처리
@BeforeAll - initAll() 사용, 테스트 케이스가 실행되기전 1번만 실행
@DisplayName - 테스트의 이름 지정
@SpringBootTest - Spring Boot 기반 Application Context 생성
@AutoConfigureMockMvc - Controller를 위한 앱의 자동 구성 작업, MockMvc를 이용하려면 필수로 추가해야함
@DataJpaTest - @Transactional을 포함하고 있어서, 하나의 테스트케이스 종료시 저장된 데이터 RollBack
@MockBean - Application Context에 있던 Bean을 Mockito Mock 객체를 생성 & 주입
@ExtendWith - Spring을 사용하지않고 Junit에서 Mockito의 기능을 사용하기 위해 추가
@Mock - 해당 필드의 객체를 Mock 객체로 생성
@InjectMocks - @InjectMocks를 설정한 필드에 @Mock으로 생성한 객체를 주입


Controller Test

package com.solo.soloProject.test;

import com.google.gson.Gson;
import com.jayway.jsonpath.JsonPath;
import com.solo.soloProject.todo.entity.Todo;
import com.solo.soloProject.todo.repository.TodoRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class ControllerTest {

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private Gson gson;
    @Autowired
    private TodoRepository todoRepository;


    /* Post Todo Test */

    @Test
    void postTodoTest() throws Exception {

        //i given RequestBody -> Json 변환
        TodoDto.Post post = new TodoDto.Post("abc",1, false);

        String content = gson.toJson(post);

        /* i when
         * Controller의 핸들러 메소드에 요청을 전송하기 위해서 perform()를 호출해야 하며,
         * perform() 내부에 Controller 호출을 위한 세부 정보 포함
         *
         * MockMvcRequestBuilders 클래스를 이용해 빌더패턴으로 HTTP Request 정보 입력 = perform
         * post() = HTTP Method + Request URL 설정
         * accpet() = 클라이언트에서 응답 받을 데이터 타입 설정
         * contentType() = 서버에서 처리 가능한 데이터 타입 지정
         * content() = Request Body 데이터 지정
         */

        ResultActions actions = mockMvc.perform(
                post("/v1/todos")
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .content(content));

        /* i then
         * when의 perform()은 ResultActions 타입의 객체를 리턴
         * 이 ResultActions 객체를 이용해 테스트로 전송한 Request에 대한 검증 수행
         * 첫 andExpect()에서 Matcher를 이용해 예상되는 기대 값 검증
         * status().isCreated() = Response Status가 201인지 검증
         *
         * header().string("", is(startsWith("URI")))에 대한 설명
         * HTTP Header에 추가된 Localtion의 문자열 값이 "/v1/todos"로 시작하는지 검증
         */
        actions.andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v1/todos"))));
    }

    /* Patch Todo Test */

    @Test
    void patchMemberTest() throws Exception {

        Todo member = new Todo("abc", 1, false);
        Todo savedTodo = todoRepository.save(member);
        long todoId = savedTodo.getTodoId();

        TodoDto.Patch patch = new TodoDto.Patch(1, "aaa", 2, true);

        String patchContent = gson.toJson(patch);

        URI patchUri = UriComponentsBuilder.newInstance().path("/v1/todos/{todo-id}").buildAndExpand(todoId).toUri();

        ResultActions actions = mockMvc.perform(patch(patchUri)
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .content(patchContent));

        actions.andExpect(status().isOk())
                .andExpect(jsonPath("$.data.title").value(patch.getTitle()))
                .andExpect(jsonPath("$.data.order").value(patch.getOrder()))
                .andExpect(jsonPath("$.data.completed").value(patch.isCompleted()));
    }

    /* Get Todo Test */

    @Test
    void getMemberTest() throws Exception {

        TodoDto.Post post = new TodoDto.Post("abc", 1, false);
        String postContent = gson.toJson(post);

        ResultActions postActions = mockMvc.perform(post("/v1/todos")
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .content(postContent));

        long memberId;

        //i postMember()의 response에 전달되는 Location Header를 가져옴 = "/v1/todos/1
        String location = postActions.andReturn().getResponse().getHeader("Location");

        //i 위에서 얻은 Location Header 값 중 memberId에 해당하는 부분만 추출
        memberId = Long.parseLong(location.substring(location.lastIndexOf("/")+1));

        //i Build Request URI
        URI getUri = UriComponentsBuilder.newInstance().path("/v11/members/{member-id}").buildAndExpand(memberId).toUri();

        mockMvc.perform(
                //i 기대 HTTP Status가 200 인지 검증
                get(getUri).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                //i Response Body의 프로퍼티가 응답으로 받은 프로퍼티의 값과 일지하는지 검증
                .andExpect(jsonPath("$.data.title").value(post.getTitle()))
                .andExpect(jsonPath("$.data.order").value(post.getOrder()));
    }

    /* Get Todos Test */
    @Test
    @DisplayName("Get Todos Test")
    public void getTodosTest() throws Exception {
        Todo todo1 = new Todo("abc", 1, false);
        Todo todo2 = new Todo("aaa", 2, false);
        Todo save1 = todoRepository.save(todo1);
        Todo save2 = todoRepository.save(todo2);

        long todoId1 = save1.getTodoId();
        long todoId2 = save2.getTodoId();

        String page = "1";
        String size = "5";
        MultiValueMap<String, String> queryparams = new LinkedMultiValueMap<>();
        queryparams.add("page", page);
        queryparams.add("size", size);

        URI getUri = UriComponentsBuilder.newInstance().path("/v1/todos").build().toUri();

        ResultActions actions = mockMvc.perform(get(getUri)
                .params(queryparams)
                .accept(MediaType.APPLICATION_JSON));

        MvcResult result = actions.andExpect(status().isOk())
                .andExpect(jsonPath("$.data").isArray())
                .andReturn();

        List list = JsonPath.parse(result.getResponse().getContentAsString()).read("$.data");

        assertThat(list.size(), is(2));
    }

    /* Delete Todo Test */

    @Test
    @DisplayName("Delete Todo Test")
    public void deleteTodoTest() throws Exception {
        Todo todo1 = new Todo("abc",1,false);

        Todo saveTodo = todoRepository.save(todo1);

        long todoId = saveTodo.getTodoId();

        URI uri = UriComponentsBuilder.newInstance().path("/v1/todos/{todo-id}").buildAndExpand(todoId).toUri();

        mockMvc.perform(delete(uri))
                .andExpect(status().isNoContent());
    }
}

Mockito Test

package com.solo.soloProject.test;

import com.google.gson.Gson;
import com.solo.soloProject.todo.entity.Todo;
import com.solo.soloProject.todo.mapper.TodoMapper;
import com.solo.soloProject.todo.service.TodoService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class MockitoTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @MockBean
    private TodoService todoService;

    @Autowired
    private TodoMapper mapper;


    /* Use Mockito Post Todo Test */

    @Test
    @DisplayName("Mockito를 이용한 Post Todo Test")
    public void mockPostTest() throws Exception {

        TodoDto.Post post = new TodoDto.Post("abc", 1, false);

        Todo todo = mapper.TodoPostToTodo(post);
        todo.setTodoId(1L);

        given(todoService.createTodo(Mockito.any(Todo.class))).willReturn(todo);

        String content = gson.toJson(post);

        ResultActions actions = mockMvc.perform(post("/v1/todos")
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .content(content));

        actions.andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v1/todos"))));
    }
}

📘 AssertThat

assertThat() 메서드는 테스트 코드에서 특정 값을 검증하기 위해 사용되는 AssertJ 라이브러리의 메서드입니다. extracting()containsExactly()assertThat() 메서드와 함께 사용되는 메서드입니다.

extracting() 메서드는 특정 객체나 속성에서 값을 추출하여 검증할 수 있도록 도와줍니다. 일반적으로 컬렉션의 요소나 객체의 속성을 추출하여 테스트하고자 할 때 사용됩니다. 예를 들어, 다음은 List 객체의 name 속성에서 값을 추출하여 검증하는 예입니다:

data class Person(val name: String, val age: Int) { 
        val people = listOf(Person("John", 25), Person("Jane", 30))  

        assertThat(people)     
                .extracting("name")     
                .containsExactly("John", "Jane")
}

위의 예제에서는 people 리스트의 각 요소에서 name 속성 값을 추출하여, containsExactly() 메서드를 사용하여 "John"과 "Jane"이 순서대로 포함되어 있는지 검증합니다.


containsExactly() 메서드는 여러 값들이 주어진 순서대로 컬렉션에 포함되어 있는지 검증합니다.

순서가 중요하며, 컬렉션에 있는 값들과 정확하게 일치해야 합니다.

다른 순서나 추가적인 값이 포함되어 있으면 검증이 실패합니다.


또 다른 예시

다음은 Map 객체에서 특정 키-값 쌍들이 포함되어 있는지 검증하는 예입니다:

val map = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3")  

assertThat(map)     
        .extracting("key1", "key2", "key3")     
        .containsExactly("value1", "value2", "value3")

위의 예제에서는 map 객체에서 "key1", "key2", "key3" 키에 해당하는 값들을 추출하여, containsExactly() 메서드를 사용하여 "value1", "value2", "value3"이 순서대로 포함되어 있는지 검증합니다.


assertThat() 메서드와 함께 extracting()containsExactly()를 사용하여 값을 검증하면, 테스트 코드를 더 읽기 쉽고 명확하게 만들 수 있습니다.