본 포스팅은, 단위 테스트 (Unit Test) 와 관련된 내용입니다.
작성된 내용 외에도 다양한 방법이 있으며, 정답은 없습니다.
<목차>
1. 서비스 단위 테스트
2. 테스트 코드
3. GitHub
SpringBoot 환경에서 전체적인 비즈니스 로직이 정상적으로 동작하는지 테스트 하기 위해서는 통합 테스트를 해야 하고, 각 모듈을 테스트 하고 싶다면, 단위 테스트를 진행해야 한다.
일반적인 Spring MVC 를 사용하는 구조라면, 일부 레이어에서만 단위 테스트를 수행하는게 무의미하거나 어려운 경우가 많다.
하지만! Mock 객체를 활용하여, 하위 레이어와 무관하게 동작하도록, 레이어 별 단위 테스트 코드를 작성해보도록 하겠다.
앞서 JPA Example 에서 작성한 소스를 기반으로 작성된 포스팅이며,
Controller - Service - DAO - Repository 구조에서, DAO 를 제외한 구조로 재구성 하였다.
2024.08.04 - [Programming/Spring & Spring boot] - [Spring Boot] JPA - 4. CRUD Example
[Spring Boot] JPA - 4. CRUD Example
1. Environment2. Sources3. Test Springboot - Basic JPA CRUD API Example JPA를 바로 사용하기 보단, 동작 원리에 대해 알고 사용하는 것이 도움이 된다.JPA 관련 포스팅https://jh4dev.tistory.com/24 [Spring Boot] JPA - 1. ORM
jh4dev.tistory.com
[서비스 단위 테스트]
- MVC 패턴에서 Service 는 비즈니스 로직을 담당하는 레이어로, 프레젠테이션 레이어(컨트롤러) 와 데이터 접근 레이어(DAO or Repository) 의 중간 레이어 이다.
- Spring 환경의 Service 단위 테스트에서 고려할 부분으로는 아래 내용이 있다.
- 단위 테스트를 위해, 외부 요인을 모두 배제
- 컨트롤러에서 서비스의 메서드를 호출한다는 가정 하에 테스트를 하는 것이며, 서비스의 비즈니스 로직을 테스트하기 위함이다.
- 서비스 메서드 내, DAO 또는 Repository 의 의존성을 주입받는 부분은 Mock 객체로 대체한다.
- Repository 객체 DI (의존성 주입)
- 스프링에 의존하는 방법과 Mock 객체를 활용하는 방법에 대해 알아볼 예정이다.
- 테스트의 목적 확인
- Assertion 을 활용하여 테스트의 목적을 달성하는지 확인할 예정이다.
- Assertion 관련 내용은, 아래를 참고
- 단위 테스트를 위해, 외부 요인을 모두 배제
2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 4 - Assertion 단정문
[Springboot] 테스트 코드 - 4 - Assertion 단정문
1. Assertion2. Jupiter Assertion 3. Jupiter Assertion Methods 테스트 코드 작성에 앞서,JUnit 라이브러리와 함께 사용할 수 있는 Jupiter Assertion 에 대해 알아보자. [ Assertion ]Jupiter Assertion 라이브러리를 알아보기
jh4dev.tistory.com
[테스트 코드]
테스트를 진행할 Service 는 다음과 같다.
import com.springboot.test.data.dto.ProductDto;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.data.entity.Product;
import com.springboot.test.data.repository.ProductRepository;
import com.springboot.test.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public ProductResponseDto getProduct(Long number) {
log.info("[getProduct()] product number = {}", number);
Optional<Product> op = productRepository.findById(number);
Product product = op.get();
log.info("[getProduct()] find product number : {}, name : {}", product.getNumber(), product.getName());
return ProductResponseDto.builder()
.number(product.getNumber())
.name(product.getName())
.price(product.getPrice())
.stock(product.getStock())
.build();
}
@Override
public ProductResponseDto saveProduct(ProductDto productDto) {
log.info("[saveProduct()] productDto : {}", productDto.toString());
Product product = Product.builder()
.name(productDto.getName())
.price(productDto.getPrice())
.stock(productDto.getStock())
.build();
Product savedProduct = productRepository.save(product);
log.info("[saveProduct()] savedProduct : {}", savedProduct.toString());
return ProductResponseDto.builder()
.number(savedProduct.getNumber())
.name(savedProduct.getName())
.price(savedProduct.getPrice())
.stock(savedProduct.getStock())
.build();
}
}
테스트할 ProductService 를 살펴보자.
- DI (의존성 주입)
- ProductRepository 를 주입받아 사용하고 있다.
- getProduct()
- 컨트롤러로부터 전달받은 Product Entity 의 Id 필드인 "number" 로 ProductRepository 를 통하여 Product 인스턴스를 조회하여 리턴한다.
- saveProduct()
- 컨트롤러로부터 전달받은 ProductDto 를 ProductEntity 로 변환한 후, ProductRepository 를 통하여 DB에 저장하고, 저장된 Product 인스턴스를 ProductResponseDto로 변환한 후 리턴한다.
<Mock 객체를 활용하는 방법>
이러한 ProductService 에 대하여, Mock 객체를 사용하는 단위 테스트 코드는 아래와 같이 작성할 수 있다.
public class ProductServiceTest {
//Repository 를 Mock 객체로 주입
private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
//ProductService
private ProductServiceImpl productService;
//주입받은 Repository 를 초기화
/**
* JUnit 생명주기 어노테이션
* BeforeEach 각 테스트 메서드가 실행되기 전에 호출되는 메서드
* BeforeAll 테스트 시작 전, 1회 호출되는 메서드
* AfterEach 각 테스트 메서드가 실행된 후에 호출되는 메서드
* AfterAll 테스트 종료 후, 1회 호출되는 메서드
* */
@BeforeEach
public void setUpTest() {
productService = new ProductServiceImpl(productRepository);
}
@Test
void getProductTest() {
//Given
Product givenProduct = Product.builder().number(123L).name("파인애플펜").price(4000).stock(200).build();
Mockito.when(productRepository.findById(123L)).thenReturn(Optional.of(givenProduct));
//When
ProductResponseDto productResponseDto = productService.getProduct(123L);
//Then
Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());
verify(productRepository).findById(123L);
}
@Test
void saveProductTest() {
//Given
Mockito.when(productRepository.save(any(Product.class))).then(returnsFirstArg());
//When
ProductResponseDto productResponseDto = productService.saveProduct(new ProductDto("연필", 1400, 420));
//Then
Assertions.assertEquals(productResponseDto.getName(), "연필");
Assertions.assertEquals(productResponseDto.getPrice(), 1400);
Assertions.assertEquals(productResponseDto.getStock(), 420);
verify(productRepository).save(any());
}
}
작성된 테스트 코드에 대해 하나씩 살펴보자.
- 외부 요인 배제
- 외부 요인을 모두 배제하기 위해, @SpringBootTest, @WebMvcTest 등의 어노테이션을 사용하지 않는다.
- Mockito.mock()
- Mockito의 mock() 메서드를 통해 ProductRepository 를 Mock 객체로 주입받았다.
- Spring 컨텍스트에 등록하지 않고 직접 객체를 초기화하여 사용하는 방식
- ProductService 초기화
- JUnit 생명주기 어노테이션인 @BeforeEach 를 사용하여,
매 테스트가 실행되기 전, ProductService 객체를, 주입받은 Mock ProductRepository 를 기반으로 초기화하여 테스트를 진행한다.
- JUnit 생명주기 어노테이션인 @BeforeEach 를 사용하여,
- Given - When - Then 패턴
- getProductTest() 메서드로 설명하겠다.
- Given
- 25번 라인
ProductService 에서 호출하고 있는 ProductRepository.findById() 메서드를 정의한다.
Mockito 의 when() 메서드를 사용하여 findById() 를 통해 전달받을 Product 엔티티 인스턴스를 정의한다.
- 25번 라인
- When
- 29번 라인
ProductService.getProduct() 테스트 진행
- 29번 라인
- Then
- 32번 라인
- When 단계에서 리턴받은 productResponseDto 에 대한 값을 검증한다.
Jupiter Assertions 을 사용하여 리턴된 결과를 검증
<스프링 컨테이너를 활용하는 방법>
@ExtendWith(SpringExtension.class) //스프링 컨텍스트를 사용하도록 설정
@Import({ProductServiceImpl.class})
public class ProductServiceTest2 {
@MockBean
ProductRepository productRepository;
@Autowired
ProductService productService;
@Test
void getProductTest() {
Product givenProduct = Product.builder().number(123L).name("파인애플펜").price(4000).stock(200).build();
Mockito.when(productRepository.findById(123L)).thenReturn(Optional.of(givenProduct));
ProductResponseDto productResponseDto = productService.getProduct(123L);
Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());
verify(productRepository).findById(123L);
}
@Test
void saveProductTest() {
Mockito.when(productRepository.save(any(Product.class))).then(returnsFirstArg());
ProductResponseDto productResponseDto = productService.saveProduct(new ProductDto("연필", 1400, 420));
Assertions.assertEquals(productResponseDto.getName(), "연필");
Assertions.assertEquals(productResponseDto.getPrice(), 1400);
Assertions.assertEquals(productResponseDto.getStock(), 420);
verify(productRepository).save(any());
}
}
각 테스트 메서드의 내용은 동일하며, 코드 상단부에 대한 설명을 추가하겠다.
- ProductRepository 초기화
- 스프링의 테스트 어노테이션을 통하여 Mock 객체를 생성하고 의존성을 주입받고 있다.
- Mock 객체를 사용한다는 점은 동일하지만, @MockBean 을 사용하는 방식은 Spring Context 에 Mock 객체를 등록하여 주입받는 형식이다.
- @ExtendWith(SpringExtension.class) 어노테이션을 사용하여, Spring Test Context 를 사용하도록 설정
- ProductService 의존성 주입을 위해, @Autowired 어노테이션을 사용하며, 이를 위해 @Import 어노테이션으로 import 해준다.
<테스트 결과>
- getProductTest()
- saveProductTest()
[GitHub]
[참고 도서]

<스프링 부트 핵심 가이드>
저자 : 장정우
출판사 : 위키북스
'Spring & Spring boot' 카테고리의 다른 글
[Spring Boot] JPA - 5. JPQL Query Method 쿼리 메서드 (0) | 2024.08.10 |
---|---|
[Springboot] 테스트 코드 - 8 - Repository Unit Test 리포지토리 단위 테스트 (0) | 2024.08.08 |
[Springboot] 테스트 코드 - 6 - Controller Unit Test 컨트롤러 단위 테스트 (0) | 2024.08.07 |
[Springboot] 테스트 코드 - 5 - Mockito (0) | 2024.08.06 |
[Springboot] 테스트 코드 - 4 - Assertion 단정문 (0) | 2024.08.06 |