JUnit5와 Mockito를 이용한 Mock Test (Java)

JUnit5, Hamcrest 및 Mockito

  • JUnit5:
  • Hamcrest: JUnit의 테스트 작성을 보다 문맥적으로 자연스럽고 우아하게 할 수 있도록 도와주는 Matcher 라이브러리입니다. JUnit5부터는 Hamcrest 관련 라이브러리가 포함되어 있지 않습니다.
  • Mockito: Java 프로그래밍에서의 단위 테스트를 위한 Mocking 오픈소스 프레임워크입니다.

앞으로의 모든 글은 JUnit5에 맞추어 기술합니다. JUnit4에 대한 내용은 특별한 경우가 아니면 언급하지 않으며, 구현 방법이 JUnit5와 상이할 수 있으므로 구체적인 세부 구현은 다른 글이나 레퍼런스를 참조하길 권장합니다.

Gradle에 Hamcrest, Mockito 라이브러리 Dependency 추가

Intellij IDEA 최신버전에서 Gradle을 이용하여 프로젝트를 구성하면 기본적으로 JUnit5 의존성을 가집니다. JUnit5부터는 Hamcrest 관련 라이브러리가 분리되었으므로 이를 Gradle 의존성에 추가해야 하며, JUnit5에서 Mockito를 함께 사용하기 위한 의존성도 함께 작성해주어야 합니다. 또한, 예제에서는 개발 편의를 위해 Lombok을 함께 사용합니다. 생성한 프로젝트의 build.gradle을 다음과 같이 수정하고, ‘Gradle Reload’ 작업을 수행하도록 합니다. 다만, Spring Boot (2.2+) 프로젝트를 구성한 경우에는 ‘org.springframework.boot:spring-boot-starter-test’ 라이브러리에 이미 JUnit5, Hamcrest 및 Mockito가 모두 포함되어 있으므로 이 작업이 필요 없습니다.

// build.gradle
plugins {
    id 'java'
}

group 'com.lattechiffon'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.20'
    annotationProcessor 'org.projectlombok:lombok:1.18.20'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testImplementation 'org.hamcrest:hamcrest-library:2.2'
    testImplementation 'org.mockito:mockito-core:3.11.2'
    testImplementation 'org.mockito:mockito-junit-jupiter:3.11.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

test {
    useJUnitPlatform()
}

이제 /src/main/java/package(예제에서는 com.lattechiffon.mockito)의 위치에 간단한 Mock 테스트를 위한 Domain(Model), Repository(DAO) 및 Service 클래스를 다음과 같이 작성합니다. Spring Boot로 프로젝트를 구성한 경우에는 어노테이션(@Repository, @Service 등)을 활용하여 Bean에 등록합니다.

  • [Domain] User: 사용자 정보를 담는 도메인 클래스입니다. 고유번호와 이름, 이메일 주소 및 권한 정보를 가집니다.
  • [Repository] UserRepository: 사용자에 대한 영속성 처리를 담당하는 인터페이스입니다. Mock Test를 위한 예제이므로 내부 구현에 대해서는 다루지 않습니다. (일반적으로 Spring Boot로 구현한 경우에는 JpaRepository<> 혹은 CrudRepository<> 인터페이스를 상속 받습니다.)
    • findByUsername: 사용자 이름을 이용하여 User 객체를 반환합니다.
    • save: User 객체를 영속화하고 해당 User 객체를 반환합니다.
    • findAll: 영속된 모든 User 객체들을 List에 넣어 반환합니다.
  • [Service] UserService: 사용자에 대한 비즈니스 로직을 가지는 클래스입니다. 클래스 내부의 인스턴스 메서드들은 각각 UserRepository 클래스의 적절한 메서드를 호출합니다. 일반적으로는 인터페이스를 상속받도록 구현(Interface: Service <- Class: ServiceImpl)하지만 여기서는 간단하게 구성했습니다.
// User.java
package com.lattechiffon.junit.mockito;

import lombok.*;

import java.util.List;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private long idx;

    @Setter
    private String username;

    @Setter
    private String email;

    @Setter
    private List<String> roles;

}
// UserRepository.java
package com.lattechiffon.junit.mockito;

import java.util.List;

public interface UserRepository {

    User findByUsername(String username);
    User save(User user);
    List<User> findAll();
}
// UserService.java
package com.lattechiffon.junit.mockito;

import java.util.List;

public class UserService {
    UserRepository userRepository;

    UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser(User user) {
        if (user.getUsername().isEmpty()) {
            return null;
        }

        return userRepository.save(user);
    }

    public User findUser(String username) {
        return userRepository.findByUsername(username);
    }

    public List<User> findAllUsers() {
        return userRepository.findAll();
    }
}

이제 구현한 서비스가 정상적으로 동작하는지 확인하기 위해 Mockito를 이용하여 Unit Test를 구현합니다. /src/main/test/package(예제에서는 com.lattechiffon.mockito)의 위치에 다음과 같이 UserServiceTest 클래스를 작성합니다.

// UserServiceTest.java
package com.lattechiffon.junit.mockito;

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 java.util.Arrays;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.CoreMatchers.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    UserRepository userRepository;

    @InjectMocks
    UserService userService;
}

여기서 세 개의 어노테이션(@ExtendWith, @Mock, @InjectMocks)이 등장합니다. 각각의 어노테이션의 의미는 다음과 같습니다.

  • @ExtendWith(MockitoExtension.class): 테스트 클래스가 Mockito를 사용함을 의미합니다.
  • @Mock: 실제 구현된 객체 대신에 Mock 객체를 사용하게 될 클래스를 의미합니다. 테스트 런타임 시 해당 객체 대신 Mock 객체가 주입되어 Unit Test가 처리됩니다.
  • @InjectMocks: Mock 객체가 주입된 클래스를 사용하게 될 클래스를 의미합니다. 테스트 런타임 시 클래스 내부에 선언된 멤버 변수들 중에서 @Mock으로 등록된 클래스의 변수에 실제 객체 대신 Mock 객체가 주입되어 Unit Test가 처리됩니다.

이를 토대로 위의 코드를 쉽게 분석할 수 있습니다. UserServiceTest는 Mockito를 사용(@ExtendWith)하고, UserRepository를 실제 객체가 아닌 Mock 객체로 바꾸어 주입(@Mock)합니다. 따라서 테스트 런타임 시 UserService의 멤버 변수로 선언된 UserRepository에 Mock 객체가 주입(InjectMocks)됩니다.

이제 Mock을 이용하여 Unit Test를 작성해보겠습니다. UserServiceTest 클래스에 다음과 같이 새로운 사용자를 등록하는 기능을 테스트하기 위한 메서드를 추가합니다. 일반적으로 Given – When – Then 패턴을 이용하여 Mock Test를 구성합니다.

  • Given: 테스트를 위한 준비 과정입니다. 변수를 선언하고, Mock 객체에 대한 정의도 함께 작성합니다.
  • When: 테스트를 실행하는 과정입니다. 테스트하고자 하는 내용을 작성합니다.
  • Then: 테스트를 검증하는 과정입니다. 예상한 값과 결괏값이 일치하는 지 확인합니다.
// UserServiceTest.java
...
@Test
    public void saveNewUser() {
        // given
        User user = User.builder().idx(1)
                .username("lattechiffon")
                .email("lattechiffon@gmail.com")
                .roles(Arrays.asList("USER", "ADMIN")).build();

        when(userRepository.save(any())).thenReturn(user); // Mock 객체 주입

        // when
        User result = userService.createUser(User.builder().username("lattechiffon").build());

        // then
        verify(userRepository, times(1)).save(any());
        assertThat(result, equalTo(user));
    }
...

위의 코드에서 주목해야 될 부분은 Mock 객체가 주입되도록 설정하는 부분입니다. when().thenReturn()을 이용하여 테스트 과정에서 UserRepository의 save 메서드를 호출하게 되면 즉시 user 인스턴스 객체를 반환하도록 만듭니다.

Gradle을 이용하여 Unit Test를 실행하면 정상적으로 통과되는 것을 확인할 수 있습니다. 즉, 작성한 테스트는 UserRepository의 save 메서드에 1번 접근하며, 정상적으로 사용자 등록이 처리됨이 검증되었습니다.

다음과 같이 추가로 테스트 메서드를 구성하여 서비스의 나머지 기능들도 정상적으로 동작하는지 확인할 수 있습니다.

// UserServiceTest.java
...
@Test
    public void findUser() {
        // given
        User user = User.builder().idx(1)
                .username("lattechiffon")
                .email("lattechiffon@gmail.com")
                .roles(Arrays.asList("USER", "ADMIN")).build();

        when(userRepository.findByUsername("lattechiffon")).thenReturn(user);

        // when
        User result = userService.findUser("lattechiffon");

        // then
        verify(userRepository, times(1)).findByUsername(any());
        assertThat(result, equalTo(user));
    }

    @Test
    public void findAllUsers() {
        // given
        List<User> users = Arrays.asList(User.builder().idx(1).username("lattechiffon").build(),
                User.builder().idx(2).username("yu-gotcha").build());

        when(userRepository.findAll()).thenReturn(users);

        // when
        List<User> result = userService.findAllUsers();

        // then
        verify(userRepository, times(1)).findAll();
        assertThat(result, equalTo(users));
    }
...

이 예제를 포함한 전체 소스코드는 아래의 GitHub 저장소에서 확인할 수 있습니다.

다음 글부터는 JUnit5를 이용한 단위 테스트에 대해 기본적인 내용들부터 하나씩 살펴보도록 하겠습니다.

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다