스프링 부트를 사용한 애플리케이션 개발에서 테스트는 매우 중요한 역할을 한다. 적절한 테스트는 코드의 안정성과 품질을 보장하고, 예기치 못한 오류를 사전에 방지할 수 있게 해 준다. 스프링에서는 다양한 테스트 방법을 제공하는데, 그중에서도 Mock을 활용한 단위 테스트와 SpringBootTest를 사용한 통합 테스트는 자주 사용되는 두 가지 방식이다.
이번에는 이 두 가지 테스트 방법의 차이점과 각각의 사용 목적에 대해 설명해보려고 한다.
@Mock
먼저 Mock을 사용한 테스트이다.
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@Mock
private UserRepository userRepository;
@InjectMocks
private ProductService productService;
@DisplayName("product 저장 테스트")
@Test
void save() {
// given
long startTime = System.currentTimeMillis();
final Long userId = 1L;
final String name = "카카오 유저 1";
final SocialType type = SocialType.KAKAO;
final User user = User.of(userId, type, name);
final ProductCreateRequest productCreateRequest = new ProductCreateRequest("안녕", ProductType.LOL, 10);
final Product product = Product.of(1L, user, productCreateRequest);
given(userRepository.getById(1L)).willReturn(user);
given(productRepository.save(any(Product.class))).willReturn(product);
// when
final Long productId = productService.createProduct(userId, productCreateRequest);
// then
assertThat(productId).isEqualTo(1L);
long endTime = System.currentTimeMillis();
System.out.println("테스트 실행 시간 : " + (endTime - startTime) + " ms");
}
}
Mock을 사용한 테스트는 37ms 정도 걸린다.
@SpringBootTest
그다음 SpringBootTest이다.
@SpringBootTest
class ProductServiceTest {
@Autowired
private UserRepository userRepository;
@Autowired
private ProductService productService;
@DisplayName("product 저장 테스트")
@Test
void save() {
// given
long startTime = System.currentTimeMillis();
final Long userId = 1L;
final SocialType type = SocialType.KAKAO;
final String name = "카카오 유저 1";
final User user = User.of(userId, type, name);
userRepository.save(user);
final ProductCreateRequest productCreateRequest = new ProductCreateRequest("안녕", ProductType.LOL, 10);
// when
final Long productId = productService.createProduct(userId, productCreateRequest);
// then
assertThat(productId).isEqualTo(1L);
long endTime = System.currentTimeMillis();
System.out.println("시간: " + (endTime - startTime) + " ms");
}
}
Mock과 다르게 100ms나 걸린다.
왜 이런 차이가 발생하는 걸까?
Mock을 사용한 테스트와 SpringBootTest를 사용한 테스트 사이의 시간 차이는 주로 테스트에서 사용하는 애플리케이션 컨텍스트의 로딩 유무와 의존성 관리 방식에서 발생한다.
Mock을 사용한 테스트 (37ms)
Mock을 사용한 테스트는 실제 스프링 컨텍스트를 로드하지 않고, 필요한 객체들만 가짜(Mock) 객체로 대체해서 테스트를 진행한다. 즉, 데이터베이스나 서비스 계층의 실제 동작을 실행하지 않고, 단순히 모의 객체가 특정 값을 반환하도록 설정해 두는 방식이다. 이렇게 하면, 전체 애플리케이션을 로드하지 않기 때문에 매우 빠른 속도로 테스트를 실행할 수 있다.
- 특징:
- 컨텍스트 미사용: 스프링의 애플리케이션 컨텍스트를 로드하지 않아서 더 가볍고 빠르게 작동한다.
- Mock 사용: @Mock과 @InjectMocks로 가짜 객체를 주입해서 테스트.
- 의존성 간단화: 실제 데이터베이스나 외부 API 호출 없이 동작을 흉내낼 수 있어서 성능 면에서 빠르다.
SpringBootTest (100ms)
SpringBootTest를 사용한 경우는 실제로 스프링 애플리케이션이 실행되듯이 모든 빈(bean)을 로드하고, 데이터베이스와 같은 외부 종속성까지 포함하여 테스트를 진행한다. 이 과정에서 애플리케이션의 전체 컨텍스트를 로드하는 데 시간이 많이 걸리기 때문에 테스트 실행 속도가 느려질 수밖에 없다.
- 특징:
- 전체 컨텍스트 로드: 스프링 애플리케이션이 실행될 때와 같이 전체 컨텍스트를 로드하고, 모든 의존성을 설정한다.
- 실제 데이터베이스 접근: 실제로 UserRepository를 통해 데이터베이스에 접근해서 데이터를 저장하니, 데이터베이스와의 I/O 작업이 추가돼 속도가 느려진다.
- 종속성 검사: 스프링 컨텍스트 내부의 빈 간 의존성까지 모두 검사하고 로드하기 때문에 시간이 더 소요된다.
그럼 Mock을 쓰는 게 속도가 빠르니까 Mock을 쓰는 게 낫겠네요!
Mock을 사용하면 테스트 속도가 훨씬 빨라지니까 무조건 Mock을 쓰는 게 낫다고 생각할 수도 있다. 하지만, 테스트의 목적에 따라 Mock을 사용하는 것이 항상 좋은 선택은 아닐 수 있다.
Mock을 사용할 때의 장점
- 속도가 빠름: 실제 의존성을 사용하지 않기 때문에 테스트가 아주 빠르게 끝난다. 특히, 반복적으로 자주 실행해야 하는 단위 테스트(Unit Test)에서 유리하다.
- 외부 종속성 제거: 데이터베이스, 파일 시스템, 네트워크 등 외부 환경에 의존하지 않아서, 특정 환경에 구애받지 않고 테스트를 진행할 수 있다.
- 로직에 집중: 특정 로직이나 메서드 동작에만 집중해서 테스트할 수 있기 때문에, 예를 들어 비즈니스 로직을 검증하기엔 Mock 테스트가 아주 좋다.
Mock의 한계
- 실제 환경과 다를 수 있음: Mock을 사용하면 외부 시스템의 동작을 흉내 내지만, 실제 데이터베이스나 다른 외부 서비스와의 상호작용을 완벽하게 묘사하지는 못한다. 그래서 통합적인 문제를 놓칠 수 있다.
- 통합 테스트의 필요성: 애플리케이션의 전체적인 동작이 잘 되는지 확인하기 위해서는, 실제 데이터베이스나 다른 서비스와의 상호작용을 포함한 통합 테스트가 필요하다. 이때는 @SpringBootTest가 적합하다.
- 의존성 주입 문제: Mock은 객체의 행동을 내가 원하는 대로 설정할 수 있지만, 실제 애플리케이션 환경에서 빈(bean)의 라이프사이클이나 의존성 주입이 제대로 동작하는지는 알 수 없다.
- 코드가 변경될 시 위험: 프로젝트 규모가 커지고, 기획이 변경되면 기존의 결과를 통해 mocking 했던 모든 테스트들은 굉장히 위험해진다. 실제로는 바뀐 상태지만, 테스트에서는 바뀌기 전이라는 괴리 속에서 테스트가 통과되면서 실제에서 문제가 발생할 확률이 높기 때문이다.
Mock vs SpringBootTest, 언제 어떤 걸 쓸까?
- Mock: 빠르게 특정 비즈니스 로직이나 메서드 단위로 동작을 테스트할 때. 외부 의존성을 제거하고 로직의 정확성만 빠르게 확인하고 싶을 때 좋다.
- SpringBootTest: 실제로 애플리케이션이 전체적으로 제대로 동작하는지 확인할 때, 특히 의존성 간의 상호작용이나 데이터베이스와의 통신을 확인하고 싶을 때는 SpringBootTest가 필요하다.
결론
Mocking은 테스트 대상에 집중할 수 있도록 해주는 강력한 도구지만, 복잡하지 않은 경우나 굳이 Mocking이 필요하지 않은 상황에서는 과도하게 사용하지 않는 것이 좋다. Mock은 빠르고 간편하지만, 모든 테스트에 적합하지는 않으며, 실제 환경을 완벽히 대체하지 못할 수도 있다.
따라서, 테스트의 목적에 맞춰 적절한 방식을 선택하는 것이 중요하다. 단위 테스트에서는 Mock을 사용해 로직에 집중하고, 통합 테스트에서는 SpringBootTest를 통해 전체 애플리케이션의 동작을 확인함으로써 균형 잡힌 테스트 전략을 세우는 것이 좋다.
'WEB' 카테고리의 다른 글
[JPA] JPA Update 실패 해결기 (0) | 2024.10.10 |
---|---|
소프트 딜리트란? (1) | 2024.09.27 |
Spring Data JPA로 된 코드를 JDBC로 다시 짜보기 (1) | 2024.09.25 |
MDC를 이용해 세부 로그 확인하기 (0) | 2024.09.23 |
스프링부트의 Tomcat과 Thread Pool (0) | 2024.09.10 |
트랜잭션이란? (0) | 2024.09.05 |
JUnit을 사용해서 Java 단위 테스트 하기 (0) | 2024.09.02 |
RDB와 NoSQL의 차이점 (3) | 2024.09.01 |