반응형
이번게시글에서는 @SpringBootTest 어노테이션을 활용한 통합테스트(상태기반 테스트) 방법을 알아보겠습니다.
어플리케이션의 모든 Bean을 로드하기에 실제 운영환경과 비슷한 환경이라는 장점이 있지만
시간, 비용의 Cost가 크다는 단점이 있습니다.
@SpringBootTest를 사용한 플로우검증을 할때는 MockBean을 사용하기 보다는 실제 DB까지 확인하는 경우가 많습니다.(상태기반 테스트)
@MockBean이나 @Mock 어노테이션은 @SpringBootTest보다는 @WebMvcTest를 사용하거나 Mock객체를 활용하여 직접 DI 하는 경우에 사용하는것이 좋습니다.(가짜객체를 사용할꺼라면 @SpringBootTest 어노테이션을 사용하여 모든 환경을 잡는것은 시간적 비용이 너무 큽니다.)
1. 어노테이션 정리
@SpringBootTest
- @WebMvcTest나 Mock 객체를 직접 주입해 테스트하는 방식과는 다르게
- 기능검증이 아닌,실제 플로우의 동작을 검증하기 위해 사용합니다.
@ActiveProfiles
- 테스트코드의 프로필을 설정해줍니다.(production, staging, develop..)
@AutoConfigureMockMvc
- 해당 어노테이션을 통해 MockMvc 객체를 주입 받을 수 있습니다.
@MockBean
- 원래의 빈 객체를 Mock객체로 대체하여 사용 할 수 있습니다.
- given, when, verify등 Mockito의 라이브러리를 활용하여 테스트할 수 있습니다.
- SpringBootTest를 사용해 테스트할때 의존하는 객체를 Mock 객체로 잡을때 사용합니다.
- 전체적인 플로우를 검증하는 @SpringBootTest 를 사용 하는 경우 보다는 WebMvcTest나 직접 DI를 하여 생성한 객체를 사용할때 MockBean SpyBean Mock 등을 사용합니다.
@Transactional
- 테스트완료후 rollback 처리를 요청하는 어노테이션입니다.
- @Commit 어노테이션과 반대되는 어노테이션입니다.
테스트코드 작성 예제입니다.
@SpringBootTest 어노테이션을 사용한 상태기반 테스트코드 예제이며
하나의 테스트로직은 DB의 상태가 정상적으로 변화했는지의 여부를 확인해줘야 됩니다.
(ex Post를 통한 데이터 등록테스트 직후 정상적으로 들어갔는지를 확인해줘야 합니다.)
2. pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
스프링부트로 프로젝트를 생성하면 기본적으로 해당 어노테이션이 포함됩니다.
해당 의존성은
junit5, AssertJ, Mockito 등의 라이브러리를 포함합니다.
3. 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);
}
}
4. 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);
}
}
5. Member.java
package com.example.junittut.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member {
private int id;
private String name;
}
6. MemberMapper.java
package com.example.junittut.repository;
import com.example.junittut.model.Member;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberMapper {
List<Member> selectAllMembers();
Member selectById(int id);
int insertMember(Member member);
int deleteMember(int id);
int updateMember(Member member);
}
7. MemberMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.junittut.repository.MemberMapper">
<select id="selectAllMembers" resultType="Member">
select * from member
</select>
<select id="selectById" resultType="Member">
select * from member where id = #{id}
</select>
<insert id="insertMember">
insert into member(name) values(#{name})
</insert>
<delete id="deleteMember">
delete from member
where id = #{id}
</delete>
<update id="updateMember">
update member set name = #{name}
where id = #{id}
</update>
</mapper>
8. 테스트코드
package com.example.junittut.controller;
import com.example.junittut.model.Member;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
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.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest {
@Autowired
MemberController memberController;
@Autowired
MockMvc mvc;
@Test
@Transactional
@DisplayName("저장, 전체조회하여 데이터가 잘 들어갔는지 확인")
void test() throws Exception {
// 1. James라는 멤버를 Post요청하여 저장합니다.
Member newMember = Member.builder().name("James").build();
String jsonData = new Gson().toJson(newMember);
mvc.perform(post("/api/member")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonData))
.andExpect(status().isCreated()); // 아래에서 바로 조회까지 할테니 상태코드정도만 확인해줍니다.
// 저장된 데이터를 단건으로 조회해도 상관없지만
// 전체조회 로직을 테스트하기 위해 전체를 조회하여 데이터가 있는지 확인해보았습니다.
MvcResult mvcResult = mvc.perform(get("/api/member"))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
String jsonResult = mvcResult.getResponse().getContentAsString();
JsonParser jsonParser = new JsonParser();
JsonArray jsonArray = jsonParser.parse(jsonResult).getAsJsonArray();
Gson gson = new Gson();
boolean isContainJames = false;
for (JsonElement jsonElement : jsonArray) {
Member member = gson.fromJson(jsonElement, Member.class);
if ("James".equals(member.getName())){
isContainJames = true;
break;
}
}
assertThat(isContainJames).isTrue();
}
@Test
@Transactional
@DisplayName("삭제, 전체데이터갯수 변화 확인")
void test2() throws Exception {
// 전체조회하여 첫번째 데이터를 꺼내옵니다.
MvcResult mvcResult1 = mvc.perform(get("/api/member"))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
String jsonResult = mvcResult1.getResponse().getContentAsString();
JsonParser jsonParser = new JsonParser();
JsonArray jsonArray = jsonParser.parse(jsonResult).getAsJsonArray();
int id = jsonArray.get(0).getAsJsonObject().get("id").getAsInt();
int size = jsonArray.size();
// 첫번째 데이터를 삭제합니다.
mvc.perform(delete("/api/member/"+id))
.andExpect(status().isOk());
MvcResult mvcResult2 = mvc.perform(get("/api/member"))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
String jsonResult2 = mvcResult2.getResponse().getContentAsString();
// 전체 데이터갯수가 한개 줄었나 확인해줍니다.
assertThat(jsonParser.parse(jsonResult2).getAsJsonArray().size()).isEqualTo(size-1);
}
@Test
@DisplayName("조회, 업데이트, 단건조회 테스트")
@Transactional
void test3() throws Exception {
// 전체조회하여 첫번째 데이터를 업데이트해줍니다.
MvcResult mvcResult1 = mvc.perform(get("/api/member"))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
String jsonResult = mvcResult1.getResponse().getContentAsString();
JsonParser jsonParser = new JsonParser();
JsonArray jsonArray = jsonParser.parse(jsonResult).getAsJsonArray();
String id = jsonArray.get(0).getAsJsonObject().get("id").getAsString();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
String newName = "Jake";
params.add("name", newName);
params.add("id",id);
mvc.perform(patch("/api/member")
.params(params))
.andExpect(status().isOk());
MvcResult mvcResult2 = mvc.perform(get("/api/member/" + id))
.andExpect(status().isOk())
.andReturn();
String response = mvcResult2.getResponse().getContentAsString();
Gson gson = new Gson();
Member updatedMember = gson.fromJson(response, Member.class);
// 업데이트후 조회된 이름과 업데이트하기로한 이름을 비교해줍니다.
assertThat(updatedMember.getName()).isEqualTo(newName);
}
}
반응형
'Spring' 카테고리의 다른 글
log4jdbc2를 사용해 쿼리 로그 설정하기(mybatis 테이블 로그) (0) | 2021.03.17 |
---|---|
[spring] mapper 어노테이션을 통한 springboot, mybatis 세팅하기 (4) | 2021.03.15 |
Junit5(Juptier) Service 테스트코드 작성법(Mock, MockBean 차이점 확인) (2) | 2021.03.13 |
Junit5(jupiter) Controller 테스트코드 작성법 (WebMvcTest, MockMvc, MockBean을 사용한 테스트) (2) | 2021.03.13 |
[java] Jsonparser, Gson 사용법 및 예제 (0) | 2021.03.05 |
댓글