관리 메뉴

제뉴어리의 모든것

[Section3] [Spring MVC] 테스팅(Testing) - 3 (Mockito) Service 계층을 끊은 Controller 테스트 본문

코드스테이츠/정리 블로깅

[Section3] [Spring MVC] 테스팅(Testing) - 3 (Mockito) Service 계층을 끊은 Controller 테스트

제뉴어리맨 2022. 9. 17. 15:03

사전에 봐야할 포스트

https://januaryman.tistory.com/458

 

깃허브 주소

https://github.com/JanuaryKim/JanuaryKim-be-template-testing

 

 

Mockito란?

Java용 테스트 프레임워크이다.

테스트를 진행할때 필요한 기능을 제공하는 프레임워크인것이다.

 

+ Mock이란?

일상에서 쓰이는 용어는 mockup 과 비슷한 의미로,

프로그래밍상에 모의 객체라는 의미이다.

mockup은 실 생활에서 "실물 모형"이란 뜻이다.

mock 또한 프로그래밍상에서 진짜같은 가짜 객체를 말한다.

 

 

Mock 객체의 필요 이유

 

  • 목 객체를 사용 안할때

  • 목 객체를 사용 할때

 

위와 같이 만약 우리가 테스트할 대상이 Controller이라면

Controller의 핸들러 메소드만 테스트 해보면 될것이다.

그런데 만약 이후 계층까지 모두 통과하여 진행하게 되면,

Controller의 테스트를 통과하기 위해 서비스 계층과 데이터 액세스 계층까지 모두 완전히 로직을 완성하고 테스트를 해야할 것이다.

그리고 Controller - Service - Repository 를 다 거치는 Controller 에 대한 테스트라고 할 수도 없을 것이다.

그리고 각 계층들만 테스트 하면 될 일을,

만약 mock을 사용하여 각각의 계층을 끊어주지 않는다면

Controller 테스트할때는 Controller - Service - Repository,

Service 테스트 할때는 Service - Repository,

Repository 테스트 할때는 Repository,

이런 과정을 거쳐서 테스트를 해야한다.

 

그러나 mock 을 사용하여 테스트 한다면

Controller 테스트할때는 Controller,

Service 테스트 할때는 Service,

Repository 테스트 할때는 Repository,

이런식으로 각 계층만 테스트할 수 있으므로 성능면에서도 좋을 것이다.

 

그럼 진행 해보자.

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.codestates'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	implementation 'org.mapstruct:mapstruct:1.4.2.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
	implementation 'org.springframework.boot:spring-boot-starter-mail'
	implementation 'com.google.code.gson:gson:2.9.0'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 

예제 코드

아래의 내용은 Controller 레벨만을 테스트 하기 위해 Service 계층을 끊어준것이다.

Service 레벨은 given 을 이용하여 당연히 정상적인 결과를 돌려주도록 하였다.

import com.codestates.member.dto.MemberDto;
import com.codestates.member.entity.Member;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
import com.codestates.stamp.Stamp;
import com.google.gson.Gson;
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.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;

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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerMockTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    // (1)
    @MockBean
    private MemberService memberService;

    // (2)
    @Autowired
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                                                        "홍길동",
                                                    "010-1234-5678");
				
        Member member = mapper.memberPostToMember(post);  // (3)
        member.setStamp(new Stamp());    // (4)

        // (5)
        given(memberService.createMember(Mockito.any(Member.class)))
                .willReturn(member);

        String content = gson.toJson(post);

        // when
        ResultActions actions =
                mockMvc.perform(
                                    post("/v11/members")
                                        .accept(MediaType.APPLICATION_JSON)
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(content)
                                );

        // then
        MvcResult result = actions
                                .andExpect(status().isCreated())
                                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                                .andExpect(jsonPath("$.data.name").value(post.getName()))
                                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))
                                .andReturn();

//        System.out.println(result.getResponse().getContentAsString());
    }
}

이전 포스트에서 설명한 애노테이션 부분은 제외하겠다.

여기서 눈여겨 볼것은 (1), (5) 이다.

 

(1) : 

import org.springframework.boot.test.mock.mockito.MockBean; 로 인해

@MockBean을 사용할 수 있는데,

MemberService 를 개발자가 만든 클래스로 빈 등록하는것이 아닌 임의의 가짜 클래스로 빈 등록을 하겠다는것이다.

말하자면 빈 이름은 MemberService이지만 그 안에 클래스 내용은 텅 비어 있는것이다.

아무것도 정의되어 있지 않다고 생각하면 된다.

그리고 @MockBean은 @SpringBootTest와 함께 쓰이는데, 그렇게 만들어진 가짜 빈을 스프링컨테이너에 넣어준다.

이것은 @Mock과 @MockBean의 결정적인 차이이다.

해당 내용은 위에 링크에 잘 나와 있으므로 이 정도로 하겠다.

 

(2) : @SpringBootTest로 인해 빈으로 정상 등록 되어있는 MemberMapper를 의존성 주입받는다

 

(3) : MemberMapper를 이용하여 DTO를 Entity로 변환

 

(4) : Member 엔티티와 1:1 연관관계가 맺어져 있는 Stamp 필드 세팅

 

(5) : 

import static org.mockito.BDDMockito.given; 로 인해 사용 가능한 give() 정적 메소드로 

텅 비어 있는 MemberService 안에 createMember() 메소드의 Input과 Output을 정해준다.

 

나머지 내용들은 이전 포스트의 내용과 거의 유사하다.

 

 

 

Stubbing이란?

Stubbing은 테스트를 위해서 Mock 객체가 항상 일정한 동작을 하도록 지정하는 것을 의미합니다