본문 바로가기
Spring

Junit5(jupiter) 테스트코드 작성법(@SpringBootTest)

by devjh 2021. 3. 14.
반응형

이번게시글에서는 @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);
    }
}
반응형

댓글