본문 바로가기
Spring

Junit5(jupiter) Controller 테스트코드 작성법 (WebMvcTest, MockMvc, MockBean을 사용한 테스트)

by devjh 2021. 3. 13.
반응형

Mock객체를 통한 행위기반 컨트롤러 테스트코드 작성법에 대해서 알아보겠습니다.

 

1. Mock 객체란?

  • 실제 객체를 만들어 사용하기에 시간, 비용 등의 Cost가 높은경우 사용
  • 가짜객체를 만들어 가짜객체가 원하는행위를 하도록 정의하고(가짜객체를 DI)
  • 타 컴포넌트에 의존하지 않는 순수한 나의 코드만 테스트하기 위해서 사용

2. 컨트롤러에서 테스트코드 작성방법

  • MockMvc를 통해 api를 호출하며 해당컨트롤러에서 의존하고 있는 객체를 Mock객체로 만들어 주입해줍니다.(@MockBean 어노테이션 사용)
  • Mock 객체는 가짜객체이므로 리턴되는값이 없습니다. 따라서 given, when 등으로 원하는 값을 리턴 하도록 미리 정의해줍니다.
  • 로직이 진행된후 해당 행위가 진행됐는지 verify를 통해 검증해줍니다.
  • 컨트로러는 @SpringBootTest와 @WebMvcTest 어노테이션을 사용하여 테스트하며 해당 포스팅에서는 @WebMvcTest를 사용합니다.(@WebMvcTest는 모든 빈을 로드하지않으므로 @MockBean을 사용합니다.)

 

 

3. 테스트코드 작성예제(SpringBoot, Mybatis)

1. pom.xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

start.spring.io에서 프로젝트를 생성하면 기본적으로 junit5(jupiter) 의존성을 포함하고있습니다.

객체와 Json과의 변환을 위해 gson 의존성을 추가해줬습니다.(Jackson의 ObjectMapper를 사용해도 상관없습니다.)

 

 

4.  MemberController.java

package com.example.junittut.controller;

import com.example.junittut.model.Member;
import com.example.junittut.service.MemberService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api")
public class MemberController {

    MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping("/member")
    public ResponseEntity<List<Member>> list(){
        List<Member> response = memberService.list();
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping("/member/{id}")
    public ResponseEntity<Member> detail(@PathVariable("id") int id) throws Exception {
        Member member = memberService.detail(id);
        return new ResponseEntity<>(member, HttpStatus.OK);
    }

    @PostMapping("/member")
    public ResponseEntity<?> insert(@RequestBody Member member) throws Exception {
        int response = memberService.insert(member);
        return new ResponseEntity<>(response,HttpStatus.CREATED);
    }

    @PatchMapping("/member")
    public ResponseEntity<?> update(@RequestParam("id") int id, @RequestParam("name") String name) throws Exception {
        Member member = new Member(id,name);
        int response = memberService.update(member);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/member/{id}")
    public ResponseEntity<?> delete(@PathVariable("id") int id) throws Exception {
        int response = memberService.delete(id);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}

테스트대상인 컨트롤러입니다.

 

 

5. MemberService.java

package com.example.junittut.service;

import com.example.junittut.model.Member;
import com.example.junittut.repository.MemberMapper;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MemberService {

    MemberMapper memberMapper;

    public MemberService(MemberMapper memberMapper) {
        this.memberMapper = memberMapper;
    }

    public List<Member> list() {
        return memberMapper.selectAllMembers();
    }

    public int insert(Member member) {
        return memberMapper.insertMember(member);
    }

    public int delete(int id) {
        return memberMapper.deleteMember(id);
    }

    public Member detail(int id) {
        return memberMapper.selectById(id);
    }

    public int update(Member member) {
        return memberMapper.updateMember(member);
    }
}

 

서비스클래스입니다.(가짜객체를 만들어 사용합니다.)

 

 

6. Memer.java

package com.example.junittut.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Member {

    private int id;
    private String name;
}

 

7. 테스트코드

package com.example.junittut.controller;

import com.example.junittut.model.Member;
import com.example.junittut.service.MemberService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
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.WebMvcTest;
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.ResultHandler;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@WebMvcTest(MemberController.class)
class MemberControllerUnitTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    MemberService memberService;

    @Test
    @DisplayName("멤버 전체조회 테스트")
    void getMemberListTest() throws Exception {
        List<Member> members = new ArrayList<>();
        members.add(Member.builder().name("John").build());

        given(memberService.list()).willReturn(members);

        mvc.perform(get("/api/member"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("John")));
    }

    @Test
    @DisplayName("멤버 추가 테스트")
    void insertMemberTest() throws Exception {
        Member member = Member.builder().name("Tom").build();
        Gson gson = new Gson();
        String content = gson.toJson(member);

        mvc.perform(post("/api/member")
                .contentType(MediaType.APPLICATION_JSON)
                .content(content))
                .andExpect(status().isCreated());

        verify(memberService).insert(member);
    }
}

 

@WebMvcTest(테스트할 컨트롤러.class)

  • 해당 클래스만 실제로 로드하여 테스트를 해줍니다.
  • 아규먼트로 컨트롤러를 지정해주지 않으면 @Controller @RestController @ControllerAdvice 등등 컨트롤러와 연관된 bean들이 로드됩니다.
  • 스프링의 모든 빈을 로드하여 테스트하는 방식인 @SpringBootTest어노테이션 대신 컨트롤러 관련 코드만 테스트하고자 할때 사용하는 어노테이션입니다.

 

@Autowired
MockMvc mvc;

  • 컨트롤러의 api를 테스트하는 용도인 MockMvc 객체를 주입받습니다.
  • perform(httpMethod)로 실행하며 andExpect, andDo, andReturn등으로 동작을 확인하는 방식입니다. 

 

@MockBean

MemberService memberService;

  • MemberController는 MemberService를 스프링컨테이너에서 주입받고있으므로
  • 가짜 객체를 만들어 컨테이너가 주입할 수 있도록 해줍니다.
  • 해당객체는 가짜객체이므로 실제 행위를 하는 객체가 아닙니다.
  • 해당 객체 내부에서 의존하는 객체와 메서드들은 모두 가짜이며 실패하지만 않을뿐 기존에 정해진 동작을 수행하지 하지 않습니다.

 

given(memberService.list()).willReturn(members);

  • 가짜객체가 원하는 행위를 할 수 있도록 정의해줍니다.(given when 등을 사용합니다.)
  • memberService의 list() 메서드를 실행시키면 members를 리턴해달라는 요청입니다.

 

andExpect(content().string(containsString("John")));

  • 리턴받은 body에 John이라는 문자열이 존재하는지를 확인합니다. 
  • given을 통해 mock객체의 예상한 행위가 정상적으로 동작했는지를 확인합니다.

 

verify(memberService).insert(member);

  • 해당 메서드가 실행됐는지를 검증해줍니다.
반응형

댓글