프로그래밍/JAVA Spring

[Spring 스프링] Mock을 이용한 테스트

hectick 2023. 4. 30. 22:54

 

 

테스트 더블이란 테스트 코드를 작성할 때, 실제 객체를 대신하여 사용하는 대체 객체를 의미한다.

 

 

 

이번에 페어 로건 덕분에 테스트 더블의 한 종류인 Mock을 처음 접했는데, 이걸 사용해 테스트를 했던 방법을 한번 기록해보았다.

(Mock이 뭔지 설명하고 싶지만 아직 Stub와 Mock의 차이를 잘 이해하지 못하였으므로 일단 생략한다.)

 

Mock을 사용하기 위해 Mockito라는 프레임워크를 이용하였다.

Mockito는 자바에서 테스트 더블을 쉽게 사용할 수 있도록 도와주는 프레임워크이다.

 

Mockito framework site

Intro Why How More Who Links Training Why drink it? Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produc

site.mockito.org

 

테스트하고 싶은 클래스

아래와 같이 Dao에 의존하는 서비스 클래스를 테스트하고 싶었다. 하지만 데이터베이스와 관련한 테스트는 이미 Dao 테스트에서 하였다. 나는 서비스에서는 Dao의 특정 메서드를 호출하면, 그 메서드의 반환한 값에 따라 서비스에서 올바른 동작을 해주는지만을 검증하고 싶었다. 아래의 코드에서는 productDao.update 메서드가 1을 반환하면 예외가 발생하지 않고, 0을 반환하면 예외가 발생하는 것을 검증하고 싶었다. 그래서 Mock을 이용하고자 했다.

    @Service
    public class ProductManagementService {

        private final ProductDao productDao;

        public ProductManagementService(final ProductDao productDao) {
            this.productDao = productDao;
        }

        public void update(final ProductDto productDto) {
            int updatedRowCount = productDao.update(ProductEntityMapper.from(productDto));
            if(updatedRowCount == 0){
                throw new IllegalArgumentException("존재하지 않는 상품입니다.");
            }
        }
    }

 

의존성 추가

mockito를 따로 의존성을 추가해주어야 한다.

    dependencies {
        testImplementation 'org.mockito:mockito-core:4.5.1'
    }

그런데 나는 스프링 부트를 사용하기 때문에 다음 의존성만으로 퉁칠 수 있다.

    dependencies {
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

 

@ExtendWith(MockitoExtension.class) 어노테이션 추가

Mock을 이용해 테스트할 클래스 위에 어노테이션을 달아주어야 한다.

이 어노테이션을 달면 아래에 나오는 @InjectMocks, @Mock을 사용할 수 있다.

	@ExtendWith(MockitoExtension.class)
	class ProductManagementServiceTest {
    
	}

 

@InjectMocks

Mock을 주입할 객체를 지정하기 위한 어노테이션이다. Mock 객체를 주입하고 싶은 대상 위에 적어준다.

        @InjectMocks
        ProductManagementService managementService;

 

@Mock

Mock으로 설정할 객체 위에 적어준다. 나는 서비스에서 사용하는 Dao를 Mock으로 만들어주었다.

        @Mock
        JdbcProductDao productDao;

 

이제 테스트를 써보자!

productDao.update 메서드가 0을 반환하도록 설정해주고 테스트를 하는 코드이다.

    @DisplayName("상품 데이터가 수정되지 않으면 예외가 발생하는지 확인한다")
    @Test
    void failTest() {
        final ProductDto productDto = ProductDto.of(3L, "pobi_doll", "https://cdn.polinews.co.kr/news/photo/201910/427334_3.jpg", 10000000);
        when(productDao.update(any())).thenReturn(0);	// Mock 객체의 특정 메서드를 호출했을 때 반환값을 0으로 지정

        assertThatThrownBy(() -> managementService.update(productDto))
            .isInstanceOf(IllegalArgumentException.class);
    }

 

만약 메서드의 반환값이 void라면 다음과 같이 코드를 써볼 수 있다.

    @DisplayName("상품 데이터가 등록되는지 확인한다")
    @Test
    void saveTest() {
        final ProductDto productDto = ProductDto.of("pobi_doll", "image", 10000000);
        final ProductDto productDto = ProductDto.of("pobi_doll", "https://cdn.polinews.co.kr/news/photo/201910/427334_3.jpg", 10000000);
        doNothing().when(productDao).insert(any());	// Mock 객체의 특정 메서드를 호출했을 때 반환값이 void일 때 사용
        managementService.save(productDto);

        assertAll(
            () -> verify(productDao, times(1)).insert(any()) // Mock 객체의 특정 메서드가 1번 호출되었는지 검증
        );
    }

 

최종 테스트 코드 형태

    @ExtendWith(MockitoExtension.class)
    class ProductManagementServiceTest {

        @InjectMocks
        ProductManagementService managementService;

        @Mock
        JdbcProductDao productDao;

        @Nested
        @DisplayName("상품을 수정하는 update 메서드 테스트")
        class UpdateTest {

            @DisplayName("상품 데이터가 수정되는지 확인한다")
            @Test
            void successTest() {
                final ProductDto productDto = ProductDto.of(1L, "pobi_doll", "https://cdn.polinews.co.kr/news/photo/201910/427334_3.jpg", 10000000);
                when(productDao.update(any())).thenReturn(1);

                assertDoesNotThrow(() -> managementService.update(productDto));
            }

            @DisplayName("상품 데이터가 수정되지 않으면 예외가 발생하는지 확인한다")
            @Test
            void failTest() {
                final ProductDto productDto = ProductDto.of(3L, "pobi_doll", "https://cdn.polinews.co.kr/news/photo/201910/427334_3.jpg", 10000000);
                when(productDao.update(any())).thenReturn(0);

                assertThatThrownBy(() -> managementService.update(productDto))
                        .isInstanceOf(IllegalArgumentException.class);
            }
        }
    }