Spring

[Spring] 테스트 클래스란?

dalooong 2023. 7. 9. 14:11

Testing

💡 테스팅이란?

저희가 만든 산출물이 기대한 대로 작동하는지를 시험해 보는 것을 .

어느 단계의 산출물을 테스트 하는지에 따라서 계층으로 나누어서 생각할 수 있다.

 

 테스트의 종류

  • 단위 테스트Unit Test : 개별 코드 단위(주로 메소드)를 테스트 하는 단계

→ 컨트롤러, 서비스, 레포지토리 계층에서 정의한 개별 메소드들이 정상적으로 작동하는지 테스트 하는 것을 의미합니다.

  • 통합 테스트 Integration Test : 서로 다른 모듈이 상호 작용 하는 것을 테스트 하는 단계

→ 컨트롤러, 서비스, 레포지토리가 전체 그림에서 유연하게 상효작용하는지를 테스트하는 것을 의미합니다.

  • 시스템 테스트 System Test : 완전히 통합되어 구축된 시스템을 테스트 하는 단계

 테스트 코드의 장점

  • 잘못된 방향의 개발을 막는다.
  • 전체적인 코드의 품질이 상승한다.
  • 최종적으로는 오류 상황에 대한 대처가 좋아져서 전체적인 개발 시간이 줄어든다.

 테스트 코드의 단점

  • 테스트 코드를 작성함으로써 개발 시간이 늘어난다.
  • 테스크코드도 유지보수가 필요해서 유지보수 비용도 늘어난다.
  • 테스트 작성법을 따로 배워야한다.

💡 H2

초기 단계의 개발 및 테스트에서 많이 활용하는, 메모리에서 동작하는 관계형 데이터베이

runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

 

Repository, Service 단위 테스트

Repository 단위 테스트 : 테스트를 관리하기 위한 클래스 작성

 @DataJpaTest
public class UserRepositoryTests {
    @Autowired
    private UserRepository userRepository;
}

@DataJpaTest : JPA 기능만 테스트 하기 위한 어노테이션

@Autowired : 의존성 주입 어노테이션

 

💡 given – when – then 패턴

: 테스트를 가독성이 좋게 작성하기 위한 패턴

 

 given : 테스트가 진행되기 위한 전제조건을 준비하는 구간

// 새로운 UserEntity 준비
        String username = "daeon.dev";
        UserEntity user = new UserEntity();
        user.setUsername(username);

 when : 테스트 하고 싶은 실제 기능을 작성하는 구간

// when; 테스트 하고 싶은 실제 기능을 작성하는 구간
        user = userRepository.save(user);

 then : when에서 받은 실행한 결과가 기대한 대로 반환되었는 검증하는 구간

	// then; 실행한 결과가 기대한 것과 같은지를 검증하는 구간
        // 1. 새로 반환받은 user의 id는 null이 아님
        assertNotNull(user.getId());
        // 2. 새로 반환받은 user의 username은 우리가 넣었던 username과 일치
        // 동일
        assertEquals(username, user.getUsername());
  • assertNotNull : 주어진 값이 null 이 아닌지를 검증합니다.
  • assertEquals : 주어진 두 값이 동일한지를 검증합니다

여기서 제시되는 테스트 이름은, @DisplayName 어노테이션을 통해 정할 수 있습니다.

 

새로운 UserEntity 생성 실패 테스트

  • given: 미리 username 을 가진 UserEntity를 생성해 둡니다.
  • when: 동일한 username 을 가진 UserEntity 의 생성을 시도합니다.
  • then: 예외가 발생합니다
@Test
    @DisplayName("새 UserEntity를 데이터 베이스에 추가 실패")
    public void testSaveNewFail() {
        //given
        String username = "daeon.dev";
        UserEntity userGiven = new UserEntity();
        userGiven.setUsername(username);
        userRepository.save(userGiven);

        //when
        UserEntity user = new UserEntity();
        user.setUsername(username);

        //when-then
        assertThrows(Exception.class, () -> userRepository.save(user));
    }

assertThrows 는 전달받은 메소드를 실행하면서, 그 과정에서 예외가 발생했는지를 검증하는 용도로 사용됩니다

 

그 외 테스트

UserService에서 UserRepository를 다양한 방법으로 사용하는 만큼, 그에 해당하는 다양한 기능을 테스트로 작성해 봅시다.

  • username으로 UserEntity 찾기
@Test
    @DisplayName("username으로 UserEntity 찾기")
    public void testFindByUsername() {
        // given: 검색할 UserEntity 미리 생성
        String username = "jeeho.dev";
        UserEntity userGiven = new UserEntity();
        userGiven.setUsername(username);
        userRepository.save(userGiven);

        // when: userRepository.findByUsername()
        Optional<UserEntity> optionalUser
                = userRepository.findByUsername(username);

        // then: Optional.isPresent(), username == username
        assertTrue(optionalUser.isPresent());
        assertEquals(username, optionalUser.get().getUsername());
    }

    // username으로 찾기 실패

    // username으로 존재하는지 확인
}

UserRepositoryTests 전체코드

package com.example.contents;

import com.example.contents.entity.UserEntity;
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.orm.jpa.DataJpaTest;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
public class UserRepositoryTests {
    @Autowired
    private UserRepository userRepository;

    //실제 진행하고자하는 코드 작성하면 됨
    // 새 UserEntity를 데이터 베이스에 추가 성공
    @Test
    @DisplayName("새 UserEntity를 데이터 베이스에 추가 성공")
    public void testSaveNew() {
        // given; 테스트가 진행되기 위한 전제조건을 준비하는 구간
        // 새로운 UserEntity 준비
        String username = "daeon.dev";
        UserEntity user = new UserEntity();
        user.setUsername(username);

        // when; 테스트 하고 싶은 실제 기능을 작성하는 구간
        user = userRepository.save(user);

        // then; 실행한 결과가 기대한 것과 같은지를 검증하는 구간
        // 1. 새로 반환받은 user의 id는 null이 아님
        assertNotNull(user.getId());
        // 2. 새로 반환받은 user의 username은 우리가 넣었던 username과 일치
        // 동일
        assertEquals(username, user.getUsername());
    }

    @Test
    @DisplayName("새 UserEntity를 데이터 베이스에 추가 실패")
    public void testSaveNewFail() {
        //given
        String username = "daeon.dev";
        UserEntity userGiven = new UserEntity();
        userGiven.setUsername(username);
        userRepository.save(userGiven);

        //when
        UserEntity user = new UserEntity();
        user.setUsername(username);

        //when-then
        assertThrows(Exception.class, () -> userRepository.save(user));
    }
    @Test
    @DisplayName("username으로 UserEntity 찾기")
    public void testFindByUsername() {
        // given: 검색할 UserEntity 미리 생성
        String username = "jeeho.dev";
        UserEntity userGiven = new UserEntity();
        userGiven.setUsername(username);
        userRepository.save(userGiven);

        // when: userRepository.findByUsername()
        Optional<UserEntity> optionalUser
                = userRepository.findByUsername(username);

        // then: Optional.isPresent(), username == username
        assertTrue(optionalUser.isPresent());
        assertEquals(username, optionalUser.get().getUsername());
    }

    // username으로 찾기 실패

    // username으로 존재하는지 확인
}

 

Service 단위 테스트

  • UserServiceTest
  • UserService는 UserRepository를 필요로 한다
  • 단위 테스트는 하나의 클래스를 격리해서 테스트 하는것을 목표로 하는 만큼, 다른 단위에서 테스트 해야되는 repository의 기능에 의존해서는 안됩니다. 이런상황에서 단위 테스트를 하기 위해서 UserRepository의 역할을 따라하는 임시객체를 만들어서 사용하는데 이 임시 객체를 Mock(모조)라고 합니다.

💡Mock 객체: Repository의 기능을 흉내 내는 모조 객체

@Mock :이 객체는 모조품, 즉 Mock 객체임을 나타내는 어노테이션 입니다.

@injectMocks : 이 객체가 필요로 하는 의존성을 정의한 Mock 객체로 전달한다는 의미의 어노테이션 입니다.

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;
		@InjectMocks
    private UserService userService;
}
  • UserDto를 받아 UserEntity 생성
@Test
    @DisplayName("UserDto로 createUser")
    public void testCreateUser(){
        //given
        //1.userRepository 전달받을 userEntity정의
        String username = "daeon.dev";
        UserEntity userEntityIn = new UserEntity();
        userEntityIn.setUsername(username);

        //2. userRepository 반환할 userEntity정의 정의
        Long userId = 1L;
        UserEntity userEntityOut = new UserEntity();
        userEntityOut.setId(userId);
        userEntityOut.setUsername(username);

        //3, userRepository.save()의 기능을 따라하도록 설정
        // = userEntityIn을 저장하게 되면, userEntityOut를 반환하게끔 설정
        when(userRepository.save(userEntityIn))
                .thenReturn(userEntityOut);
        when(userRepository.existsByUsername(username))
                .thenReturn(false);
        //when
        UserDto userDto = new UserDto();
        userDto.setUsername(username);
        UserDto result = userService.createUser(userDto);
        //then
        assertEquals(userId, result.getId());
        assertEquals(username, result.getUsername());
    }

UserServiceTest 전체코드

package com.example.contents;

import com.example.contents.dto.UserDto;
import com.example.contents.entity.UserEntity;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    // UserDto(id!=null)를 입력받아 UserDto(id!=null)을 반환
    @Test
    @DisplayName("UserDto로 createUser")
    public void testCreateUser(){
        //given
        //1.userRepository 전달받을 userEntity정의
        String username = "daeon.dev";
        UserEntity userEntityIn = new UserEntity();
        userEntityIn.setUsername(username);

        //2. userRepository 반환할 userEntity정의 정의
        Long userId = 1L;
        UserEntity userEntityOut = new UserEntity();
        userEntityOut.setId(userId);
        userEntityOut.setUsername(username);

        //3, userRepository.save()의 기능을 따라하도록 설정
        // = userEntityIn을 저장하게 되면, userEntityOut를 반환하게끔 설정
        when(userRepository.save(userEntityIn))
                .thenReturn(userEntityOut);
        when(userRepository.existsByUsername(username))
                .thenReturn(false);
        //when
        UserDto userDto = new UserDto();
        userDto.setUsername(username);
        UserDto result = userService.createUser(userDto);
        //then
        assertEquals(userId, result.getId());
        assertEquals(username, result.getUsername());
    }
}

@ExtendWith : Mock 객체를 만들기 위해서 Mockito를 사용한다는 부분을 첨부한 어노테이션


Controller 단위, 통합 테스트

  • UserControllerTest
  1. 테스트 준비
//UserService 만들기 Mocking
@ExtendWith(MockitoExtension.class)
public class UserControllerTest {
    @Mock
    private UserService userService;

    @InjectMocks
    private UserController userController;

    //Controller가 있을 때 HTTP 요청이 보내졌다 가정해주는 객체
    private MockMvc mockMvc;

    @BeforeEach
    public void beforeEach(){
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }
}

MockMvc MockMvcBuilder.standalonSetup(userController).build() 를 사용하면 UserController 를 테스트 하기 위한 엔드포인트만 설정한 서버를 Mock 합니다.

그리고 이를 실행하는 @BeforeEach 어노테이션이 붙은 메소드는 각 단위 테스트 이전에 mockMvc 가 초기화 되게 합니다.

그 외 UserService  @Mock  UserController @InjectMocks 는 이전과 동일하게 동작합니다.

 

2. 테스트 코드 수정

  • UserControllerTest 전체코드
package com.example.contents;

import com.example.contents.dto.UserDto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

//UserService 만들기 Mocking
@ExtendWith(MockitoExtension.class)
public class UserControllerTest {
    @Mock
    private UserService userService;

    @InjectMocks
    private UserController userController;

    // Controller가 있을때 HTTP 요청이 보내졌다 가정해주는 객체
    private MockMvc mockMvc;

    @BeforeEach
    public void beforeEach() {
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

    @Test
    @DisplayName("UserDto를 나타내는 JSON 요청을 보내면 id가 null이 아닌 UserDto JSON 응답")
    public void testCreate() throws Exception {
        // given
        // 1. userService.createUser 에 전달한 UserDto 준비
        String username = "jeeho.dev";
        UserDto requestDto = new UserDto();
        requestDto.setUsername(username);
        // 2. userService.createUser 가 반환할 UserDto 준비
        Long userId = 1L;
        UserDto responseDto = new UserDto();
        responseDto.setUsername(requestDto.getUsername());
        responseDto.setId(userId);
        // 3. userService.createUser 의 동작 가정
        when(userService.createUser(requestDto))
                .thenReturn(responseDto);

        // when
        // perform: HTTP 요청을 보낸것을 시뮬레이션 하여 UserController 에게
        ResultActions result = mockMvc.perform(
                // 요청의 형태(Body 라던지)를 빌더처럼 정의
                post("/users")
                        .content(JsonUtil.toJson(requestDto))
                        .contentType(MediaType.APPLICATION_JSON));

        // then
        result.andExpectAll(
                status().is2xxSuccessful(),  // 상태코드가 200
                content().contentType(MediaType.APPLICATION_JSON),  // 응답이 JSON 형태로
                jsonPath("$.username", is(username)),  // username은 요청한 값 그대로
                jsonPath("$.id", notNullValue())  // id는 null은 아닌 값
        );
        mockMvc.perform(
                        // 요청의 형태(Body 라던지)를 빌더처럼 정의
                        post("/users")
                                .content(JsonUtil.toJson(requestDto))
                                .contentType(MediaType.APPLICATION_JSON))
        .andExpectAll(
                status().is2xxSuccessful(),  // 상태코드가 200
                content().contentType(MediaType.APPLICATION_JSON),  // 응답이 JSON 형태로
                jsonPath("$.username", is(username)),  // username은 요청한 값 그대로
                jsonPath("$.id", notNullValue())  // id는 null은 아닌 값
        );
    }
}