반응형
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);
- 해당 메서드가 실행됐는지를 검증해줍니다.
반응형
'Spring' 카테고리의 다른 글
Junit5(jupiter) 테스트코드 작성법(@SpringBootTest) (4) | 2021.03.14 |
---|---|
Junit5(Juptier) Service 테스트코드 작성법(Mock, MockBean 차이점 확인) (2) | 2021.03.13 |
[java] Jsonparser, Gson 사용법 및 예제 (0) | 2021.03.05 |
[spring] spring과 객체지향의 원리 (0) | 2021.02.28 |
[spring] 객체지향 설계의 5가지 원칙 with spring (SOLID) (0) | 2021.02.22 |
댓글