본문 바로가기

Spring

[Spring] 단위 테스트

단위 테스트

프로그램의 작은 단위의 테스트를 진행하는 것을 의미합니다. 작은 단위의 기준은 정해져있지 않지만 보통은 메서드 단위의 테스트를 진행합니다.

FIRST

좋은 단위 테스트라고하면 무조건 따라오는 5가지 특징이 있습니다.

  • Fast : 단위 테스트는 빨라야합니다. 단위테스트의 목적이 빠르게 피드백주는데 있기 때문입니다.
  • Isoated : 단위 테스트는 독립적이어야합니다. 말 그대로 "단위" 에 집중하는 것이기 때문에 다른 계층의 영향을 받으면 안됩니다.
  • Repeatable : 단위 테스트의 결과는 항상 같아야합니다. DB 초기값이 변하거나 시간의 유동성에 따라 다른 결과를 낸다면 그때마다 다른 테스트 코드를 작성해야합니다. 따라서 결과가 항상 같게 코드를 구성해야합니다.
  • Self-validating : 단위 테스트는 기대하는 결과에 대해 단언(assert) 해야합니다.
  • Timely : 테스트는 적시에 작성해야합니다. 단위 테스트는 실제 코드를 구현하기 직전에 구현해야합니다.

Mock

테스트 코드를 한번이라도 보신분들은 Mock이라는 단어를 접했을겁니다. Mock을 제대로 이해하지 못하고 테스트를 짜면 정말 고생 많이하므로 자세하게 배우고 코드를 짜는것을 권합니다.

 

Mock 보다 먼저 알아야될게 있는데 바로 @SpringBootTest 입니다.

@SpringBootTest의 기능중엔 모든 Bean들을 생성하는 작업이 있는데 그렇기 때문에 정말 오래걸리는 작업입니다.

나는 독립적으로 다른 계층의 영향을 받지않고 Bean이 하나만 존재하면 되는 테스트를 진행하려하는데 @SpringBootTest 을 사용하면 모든 Bean을 생성해야하는 리스크가 있습니다.

그렇기때문에 우리는 Mock이라는 객체를 이용해서 @SpringBootTest의 도움을 받지않고 단위 테스트를 진행해야합니다. (필요에 따라 사용해도 단위 테스트라는 말이 깨지는 것은 아닙니다.)

 

Mock 객체를 사용하기 편하게 만들어준 라이브러리가 Mockito 입니다.

@ExtendWith(MockitoExtension.class) 로 Mockito 라이브러리를 사용할 수 있습니다.

 

Mock 이란 가짜객체입니다. 실제 객체를 만들어 사용하기에 비용이 많이 들때 사용하는 방법입니다.

따라서 Mock 객체로 만들어진 변수는 원하는 실행결과를 만들어 내지 못할겁니다.

이럴때 Mockito 라이브러리를 사용합니다. 

 

@Test
@DisplayName("회원가입_성공")
@Transactional
void 회원가입_성공(){
    // mocking
    BDDMockito.given(userRepository.save(any()))
            .willReturn(user);
    BDDMockito.given(userRepository.existsByUsername(any()))
            .willReturn(false);
    BDDMockito.given(userRepository.existsByEmail(any()))
            .willReturn(false);
    BDDMockito.given(userRepository.existsByTelephone(any()))
            .willReturn(false);

    // when
    Boolean result = userService.create(param);

    // then
    Assertions.assertEquals(true, result);
}

 

위와같이 Mock으로 생성된 객체에 대해 입력값과 결과값을 정해주는 작업을 하게되면 현재 Service Layer의 테스트를 진행중일때 Repository의 영향을 받지 않을 수 있습니다. 이 작업을 Mocking 이라고 표현합니다.

 

BDDMockito

분명 Mockito를 사용하라했는데 위 코드를 보면 BDDMockito를 사용했습니다. 둘의 차이가 무엇일까요?

BDD란 행위 주도 개발을 의미합니다. 코드를 한번 볼까요?

 

 

이는 BDDMockito 클래스의 일부인데 Mockito를 상속받았습니다. 둘이 다른게 아니라 기능확장을 했겠구나라는 것을 알 수 있습니다.

그럼 어떤 기능을 확장했을까요? 결과부터 이야기하면 시나리오에 맞게 테스트 코드가 읽힐 수 있도록 이름을 변경했습니다.

여기서 시나리오라고 하는것에 대해 알아보고 갈게요.

 

given : 보통 테스트를 준비하는 단계입니다. 테스트에 사용하는 변수들을 정의하거나 Mocking 작업을 하는 과정입니다.

when : 실제 행동을 하는 단계입니다. 위의 코드에서는 userService.create 를 동작시키는 단계입니다.

then : 테스트를 검증하는 단계입니다. 예상한 값, 실제 값을 Assertions 라이브러리를 이용해 검증합니다.

 

기존 Mockito 클래스는 아래와같이 행동에 대한 코드가 달라 가독성을 해쳤습니다.

 

Mockito.when(userRepository.save(any()))
                .thenReturn(user);
BDDMockito.given(userRepository.save(any()))
                .willReturn(user);

 

따라서 이를 보완해서 나온것이 BDDMockito 입니다.

 

@Mock, @InjectMocks, @Spy

Mockito 라이브러리를 사용해 Mock 객체를 정의하면 해당 변수에 어떤 어노테이션을 붙여야할지 헷갈립니다.

 

@Mock

말 그대로 가짜 객체입니다. 해당 객체의 메소드를 사용하려면 스터빙이 필요합니다. 만일 스터빙 하지 않고 그냥 호출한다면 Primitive 타입은 0을 Reference 타입은 Null을 반환합니다. 여기서 스터빙이란 위에서 말한 Mocking이라고 생각하면 됩니다.

 

 

Primitive 타입이 궁금하다면 아래글을 읽어주세요!

 

[Java] Wrapper

Wrapper 자바에는 크게 2가지 자료형으로 구분할 수 있습니다. 기본 타입인 Primitive Type 과 참조 타입인 Reference Type 입니다. 여기서 기본타입은 int, double, float, boolean등이 있는데 이를 객체로 표현화

mirr-coding.tistory.com

 

 

@Spy

진짜 객체입니다. 메소드 실행 시 스터빙하지 않으면 기존 객체의 로직을 그대로 수행한 값을 리턴합니다. 스터빙을 했을땐 스터빙 값을 리턴합니다.

 

 

@InjectMock

@Mock이나 @Spy로 생성된 Mock객체들을 의존성 주입하는 어노테이션입니다.

 

실제 UserService에는 다음과 같이 정의 되어있다고 가정합니다.

 

@Service
public class UserServiceImpl implements UserService {
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final UserRepository userRepository;

    @Autowired
    public UserServiceImpl(BCryptPasswordEncoder bCryptPasswordEncoder, UserRepository userRepository) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.userRepository = userRepository;
    }
}

 

그랬을때 아래와 같은 코드는 UserRepository와 BCryptPasswordEncoder에 의존성을 주입해줍니다.

 

class UserServiceImplUnitTest extends BaseServiceImplUnitTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UserRepository userRepository;

    @Mock
    private BCryptPasswordEncoder bCryptPasswordEncoder;
}

 

따라서 보통 테스트하고 있는 클래스에 많이 붙는 어노테이션입니다.

 

 

또한 @MockBean@SpyBean 도 존재하는데요.

 

@MockBean

 

MockBean은 @Mock과 유사하게 동작하는데, 다른점이 존재합니다.

@Mock 으로 정의된 객체는 오직 @InjectMocks으로만 의존성을 주입받습니다.

 

하지만 @MockBean 은 Mock 객체를 스프링 컨텍스트에 등록하는 것이기 때문에 @SpringBootTest를 통해서 의존성이 주입되게됩니다.

 

 

@SpyBean

 

@MockBean과 비슷하게 스프링 컨텍스트에 등록되어 의존성이 주입되는 방식이지만, @Spy에서 설명드렸듯이 실제 객체의 로직을 그대로 수행하기 때문에 @SpyBean으로 정의된 클래스가 인터페이스라면 꼭 인터페이스를 구현한 구현체가 존재하여야합니다.

'Spring' 카테고리의 다른 글

[Spring] Session (feat. 프로젝트 경험)  (0) 2023.04.28
[Spring] application-{환경}.yml 과 @Value  (0) 2023.04.17
[Spring] Bean  (0) 2023.02.17
[Spring] Spring MVC  (0) 2023.02.16
[Spring] @Transactional  (0) 2023.02.16