글을 쓰게 된 이유

프로젝트를 진행하면서 제가 만들었던 Repository, Service, Controller가 작동하는지 안하는지 각각의 기능들을 제각각 테스트를 할 필요를 느꼈습니다. 모든 기능을 만들때마다 서버를 킨다음에 직접 테스트하기에는 시간이 오래걸리고, 여러 기능이 묶여있는 함수들을 테스트할경우 어떤 것이 문제인지 바로바로 알기 어렵습니다.

또한 이러한 테스트코드들은 이후에 유지보수를 하거나 오류의 원인을 찾는데 매우 중요한 역할을 합니다.

이러한 점들을 극복하기 위하여 JUnit과 Mock을 활용하여 단위테스트를 진행해보려고 합니다.

 

단위테스트란 무엇인가?

함수, 메서드, 클래스 등과 같이 독립적으로 테스트 가능한 작은 부분을 독립적으로 테스트하는 것을 의미합니다.

  • Repository 에서는 Repository 만의 기능을 수행합니다.
  • Service에서는 Service 만의 기능을 수행합니다. (Repository 는 Mock 가짜객체를 생성하여 테스트합니다.)
  • Controller에서는 Controller만의 기능을 수행합니다. (Service 는 Mock 가짜객체를 생성하여 테스트합니다.)

즉, DB와의 접촉을 가짜로 하여 데이터베이스에 의존하지 않고 테스트할 수 있습니다.

저같은경우 단위테스트를 위하여 JUnit 테스트 프레임워크를 사용하고, Mock을 활용하여 가짜객체를 만들어서 테스트합니다.

 

JUnit의 LifeCycle

JUnit을 사용하기 전에 JUnit의 LifeCycle 관련 어노테이션을 알아보았습니다.

  • @BeforeAll :
    • 테스트 클래스 인스턴스를 초기화할때 가장 먼저 실행됩니다.
    • 테스트 클래스에 포함된 테스트 메서드가 여러개 있어도 한번만 실행됩니다.
    • 객체를 생성하기 전에 미리 실행하므로 static 접근 제어자를 정의하여 사용합니다.
  • @BeforeEach :
    • 테스트 메서드가 실행되기 전에 한번 테스트 클래스마다 실행됩니다.
  • @AfterEach :
    • 테스트 메서드가 실행된 후테스트 클래스마다 실행됩니다.
  • @AfterAll :
    • 테스트 클래스의 모든 테스트 메서드가 실행을 마치면 마지막으로 한번만 실행됩니다. 이또한 static 접근제어자를 정의하여 사용합니다.

 

JUnit의 주요 Annotation

JUnit Test 프레임워크를 사용하기 위한 어노테이션입니다.

  • @SpringBootTest :
    • 통합테스트의 용도로 사용됩니다.
    • @SpringBootApplicaiton(Server 시작점)의 모든 하위의 Bean들을 스캔하여 로드합니다.
    • 그 후 Test에 사용할 Application Context를 만들어 Bean을 추가하고, MockBean을 찾아 교체합니다.
  • @ExtendWith :
    • Main으로 사용될 Class를 지정할 수 있습니다.
    • @SpringBootTest는 기본적으로 @ExtendWith가 설정되어 있으므로 따로 설정할 필요는 없습니다.
    • JUnit4 에서는 @RunWith 어노테이션이었던 것이 변경되었습니다.
  • @WebMvcTest(Class명.class) :
    • 매개변수(Class명.class)에 작성된 클래스만 실제로 로드하여 테스트를 지정합니다.
    • 매개변수를 지정해주지 않으면 @Controller, @RestController, @RestControllerAdvice 와 같은 컨트롤러와 연관된 모든 Bean들이 모두 로드됩니다.
    • Spring의 모든 Bean을 로드하는 @SpringBootTest 대신 Controller 관련 코드만 테스트할경우 사용합니다.
  • @Autowired about Mockbean
    • Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입받습니다.
    • perform() 메소드를 활용하여 컨트롤러의 동작을 확인할 수 있습니다.
    • .andExpect() : 기대값 설정, andDo() : 어떤 행동을할지 설정, andReturn() : 반환값 설정
  • @MockBean
    • 테스트할 클래스에서 주입받고 있는 객체에 대해 가짜 객체를 생성해줍니다.
    • 예시로 들면, Service가 만약 Repository를 사용한다고 하였을때 의존성으로 Repository가 잡혀있습니다. MockBean이라는 어노테이션을 통해 가짜 객체의 동작에 대해 정의하여 사용할 수 있습니다. 이떄 given() 메소드를 활용하여 진행합니다.
  • @AutoConfigureMockMvc
    • spring.test.mockmvc의 설정을 로드하여, MockMvc의 의존성을 자동으로 주입하여 사용합니다.
    • MockMvc는 REST API를 테스트할 수 있는 클래스입니다.

 

Spring JUnit Test FrameWork 구현해보기

1. Gradle에 Import

testImplementation 'org.springframework.boot:spring-boot-starter-test'

 

  • Test 관련 모든 의존성을 부여받고 진행합니다. ( 아래 한가지의 implementation 으로 대부분의 JUnit Test를 사용가능합니다.)

2. MemberRepository.class

  • Repository  관련 테스트입니다. Repository 는 직접 DB와 상호작용하는 곳이기에 Mock을 사용하는것보다는 실제로 DB와 상호작용하는 테스트를 합니다.
@SpringBootTest
public class MemberRepositoryTests {
    @Autowired
    private MemberRepository memberRepository;

    private Member member;
    private final String member_id= "hello@naver.com";
    private final String member_password= "123123123";
    private final String member_nickname = "hello";
    private final boolean member_from_social = false;


    @BeforeEach
    public void setup() throws Exception{
        if(memberRepository.countByMember_id(member_id) == 0){
            member = memberRepository.save(
                    Member.builder()
                            .member_id(member_id)
                            .member_password(member_password)
                            .member_nickname(member_nickname)
                            .member_from_social(member_from_social)
                            .build()
            );
        }

    }

    @DisplayName("findByMember_id Test")
    @Test
    public void testGetWithMember_id(){
        // given, when
        Optional<Member> member = memberRepository.findByMember_id(member_id);

        // then
        assertNotNull(member.get());
        System.out.println(member);
    }




}

 

3. MemberServiceTests.class

  • CRUD 관련 모든 테스트 클래스를 작성했습니다.
  • MemBerService는 실제로 사용할 객체이기에 Bean 을 가져오고, MemberService에 선언된 MemberRepository 는 Mock으로 가짜객체를 통하여 진행합니다.
@SpringBootTest(classes = {MemberServiceImpl.class})
public class MemberServiceTests {
    @MockBean
    private MemberRepository memberRepository;

    @Autowired
    private MemberService memberService;

    @DisplayName("Member Service Register Test")
    @Test
    public void registerMemberTest() throws DuplicateMemberException {
        // given
        Member member = Member.builder()
                .member_no((long)123)
                .member_id("member_id")
                .member_password("member_password")
                .member_nickname("member_nickname")
                .member_from_social(false)
                .build();

        Mockito.when(memberRepository.save(refEq(member))).thenReturn(member);

        // when
        MemberDTO memberDTO = MemberDTO.builder()
                .member_no((long)123)
                .member_id("member_id")
                .member_password("member_password")
                .member_nickname("member_nickname")
                .member_from_social(false)
                .build();
        Long result = memberService.register(memberDTO);

        // then
        Assertions.assertEquals(result, 123);
        verify(memberRepository).save(refEq(member));
    }

    @DisplayName("Member Service Get Test")
    @Test
    public void getMemberTest(){
        // given
        Member existingMember = Member.builder().member_no((long)123).member_id("member_id").member_password("member_password").member_nickname("member_nickname").member_from_social(false).build();
        Mockito.when(memberRepository.findById((long) 123)).thenReturn(Optional.of(existingMember));

        // when
        MemberDTO memberDTO = memberService.get((long)123);

        // then
        Assertions.assertEquals(memberDTO.getMember_no(), 123);
        Assertions.assertEquals(memberDTO.getMember_id(), "member_id");
        Assertions.assertEquals(memberDTO.getMember_password(), "member_password");
        Assertions.assertEquals(memberDTO.getMember_nickname(), "member_nickname");
        Assertions.assertEquals(memberDTO.isMember_from_social(), false);

        verify(memberRepository).findById((long) 123);
    }

    @DisplayName("Member Service Modify Test")
    @Test
    public void modifyMemberTest() {
        // given
        Member existingMember = Member.builder().member_no((long)123).member_id("member_id").member_password("member_password").member_nickname("member_nickname").member_from_social(false).build();

        Mockito.when(memberRepository.findById(123L)).thenReturn(Optional.of(existingMember));

        MemberDTO memberDTO = MemberDTO.builder().member_no(123L).member_nickname("modified_member_nickname").build();
        // when
        memberService.modify(memberDTO);

        // then
        Mockito.verify(memberRepository).save(Mockito.any(Member.class));
        Assertions.assertEquals("modified_member_nickname", existingMember.getMember_nickname());
    }

    @DisplayName("Member Service Soft Remove Test")
    @Test
    public void removeTest() {
        // given
        LocalDateTime del_dt = LocalDateTime.now();
        Member existingMember = Member.builder().member_no((long)123).member_id("member_id").member_password("member_password").member_nickname("member_nickname").member_from_social(false).del_dt(del_dt).build();
        Mockito.when(memberRepository.findById(123L)).thenReturn(Optional.of(existingMember));

        // when
        memberService.remove(123L);

        // then
        Mockito.verify(memberRepository).deleteByMember_no(123L);
        Assertions.assertNotNull(memberRepository.findById(123L).get().getDel_dt());
    }

}

 

4. MemberControllerTests.class

  • Rest API 를 테스트하기 위한 Test입니다.
  • MockMvc를 활용하기 위해 @AutoConfigureMockMvc를 반드시 사용해야합니다.
  • ObjectMapper를 활용하는 이유는  자바 객체와 JSON 데이터 간의 변환을 처리를 하기 위하여입니다.
  • .andExpect(jsonPath("$").value(123L)) 부분은  https://github.com/json-path/JsonPath JSONPATH 표현식을 보면 쉽게 이해할 수 있습니다. 반환된 JSON 객체를 가져온다는 의미입니다. 여기서는 한가지의 JSON 만 나오기에 $.member_no 가 아닌 $ 로 가져옵니다.
@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerTests {

    @Autowired
    private MockMvc mockMvc;

    // body에 json 형식으로 회원의 데이터를 넣기 위해서 Map을 이용한다.
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    MemberService memberService;

    @Test
    @DisplayName("Register a new member")
    void registerMemberTest() throws Exception {
        // Given
        MemberDTO memberDTO = MemberDTO.builder()
                .member_no(123L)
                .member_id("member_id")
                .member_password("123123123")
                .member_nickname("member_nickname")
                .member_from_social(false)
                .build();
        when(memberService.register(memberDTO)).thenReturn(123L);

		// When
        mockMvc.perform(post("/member")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(memberDTO)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").value(123L))
                .andDo(print());

        // Then
        verify(memberService).register(memberDTO);
    }

    @Test
    @DisplayName("Register a new member")
    void getMemberTest() throws Exception {
        // Given
        MemberDTO memberDTO = MemberDTO.builder()
                .member_no(123L)
                .member_id("member_id")
                .member_password("123123123")
                .member_nickname("member_nickname")
                .member_from_social(false)
                .build();
        Long member_no = 123L;
        when(memberService.get(member_no)).thenReturn(memberDTO);

	// When
        mockMvc.perform(get("/member/"+member_no)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(memberDTO)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.member_no").value(123L))
                .andExpect(jsonPath("$.member_id").value("member_id"))
                .andExpect(jsonPath("$.member_password").value("123123123"))
                .andExpect(jsonPath("$.member_nickname").value("member_nickname"))
                .andExpect(jsonPath("$.member_from_social").value(false))
                .andDo(print());

        // Then
        verify(memberService).get(member_no);

    }
}

 

마무리

이렇게 JUnit 과 Mock을 활용하여 단위테스트를 진행해보았습니다.

Test 관련하여 궁금한 점이 해소되었길 바랍니다.

 

 

참고자료

+ Recent posts