세 줄 요약
- 리팩토링 과정에서 결과의 변경 유무를 테스트 코드를 통해 확인해야 한다.
- @SpringBootTest는 통합 테스트에 사용되는 어노테이션으로, 두 개 이상의 클래스가 협력할 때 사용한다.
- Assertion 클래스의 메소드를 통해 로그 확인없이 테스트의 통과 여부를 확인할 수 있다.
리팩토링과 테스트 코드
리팩토링
리팩토링은 '동일한 입력에 대해 결과의 변경 없이 코드 구조가 개선되는 것'을 의미합니다.
코드의 구조가 개선되었지만 결과 또한 변경되었다면 리팩토링이라고 볼 수 없습니다.
따라서 테스트 코드를 통해 리팩토링 과정에서 결과가 변경된 건 아닌지 확인해야 합니다.
테스트 코드
테스트의 종류에는 통합 테스트와 단위 테스트가 있습니다.
통합 테스트
2개 이상의 클래스가 협력하여 특정 기능을 정상적으로 수행하는지를 테스트하는 것
→ 서비스에서 DB에 값이 저장되거나, 기존에 있는 값을 조회하는 등의 기능이 정상적으로 동작하는지 확인할 때
스프링 어플리케이션이 실행되어 실제로 객체를 만들어서 테스트를 진행
→ 테스트 진행 시 어플리케이션이 시작되는 시간이 소모됨
단위 테스트
메소드와 같이 작은 단위에서 수행되는 테스트를 의미
테스트 타켓이 되는 메소드에서 사용하는 다른 클래스의 메소드는 모킹하여 테스트 진행
모킹된 테스트의 구현 여부와 관계 없이 타겟 메소드만 테스트 가능
스프링 어플리케이션이 실행될 필요가 없어 테스트 속도가 빠름
이번 포스팅에서는 통합 테스트에 관한 내용을 정리해보겠습니다.
단위 테스트에 대한 내용은 해당 게시글에서 확인 할 수 있습니다.
테스트 코드 작성
간단한 상품 관리 어플리케이션을 통한 예제입니다.
새로운 상품을 생성하고 id로 조회하는 경우에 대한 테스트를 실시합니다.
ProductListRepository
우선 로컬 개발 환경에서 사용하는 test profile을 ActiveProfiles로 등록하고 테스트를 실시합니다.
테스트 하고자 하는 클래스(서비스 클래스)에서 마우스 우클릭 → Generate → Test 순서로 테스트 클래스를 생성합니다.
메소드는 선택하지 않고 생성합니다.
테스트 타겟이 되는 클래스와 동일한 디렉토리를 갖는 테스트 클래스가 생성된 것을 확인할 수 있습니다.
소스 코드
(생략)
// import static : static 메소드들을 클래스 이름 없이 바로 사용할 수 있다.
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // 스프링 컨테이너가 뜨는 통합 테스트를 위한 어노테이션
@ActiveProfiles("test") // @Profile("test") 어노테이션이 붙은 클래스(ProductListRepository)를 빈으로 등록
class ProductServiceTest {
@Autowired // 테스트 코드에선 필드 주입방식으로 진행해도 무방
ProductService productService;
@Test // 아래 내용이 테스트 코드임을 나타내는 어노테이션
@DisplayName("상품을 추가한 후 id로 조회하면 해당 상품이 조회되어야 한다.") // 해당 어노테이션을 통해 테스트의 코드의 이름을 지정
void productAddAndFindById() { // 메소드 명은 한글로 하는 경우도 있다.
ProductDTO productDTO = new ProductDTO("pencil", 300, 20);
ProductDTO savedProductDTO = productService.add(productDTO);
Long savedProductId = savedProductDTO.getId();
ProductDTO foundProductDTO = productService.findById(savedProductId);
// 로그를 확인하지 않고 테스트 통과 여부를 확인하기 위해 assertTrue 메소드 사용
// import static으로 인해 클래스 이름 없이 바로 메소드 사용
assertTrue(savedProductDTO.getId() == foundProductDTO.getId());
assertTrue(savedProductDTO.getName() == foundProductDTO.getName());
assertTrue(savedProductDTO.getPrice() == foundProductDTO.getPrice());
assertTrue(savedProductDTO.getAmount() == foundProductDTO.getAmount());
}
}
실행 결과
ProductService의 findById() 메소드의 리팩토링 과정에서 문제가 발생한 경우
assertTrue() 메소드 대신 System.out.println() 메소드를 통해 테스트를 진행할 수도 있습니다.
하지만 그런 경우 테스트 코드는 항상 통과되기 때문에 로그를 일일이 확인해야 하는 번거로움이 있습니다.
ProductDatabaseRepository
서비스 환경에서 사용하는 prod profile을 ActiveProfiles로 설정하고 테스트를 진행합니다.
소스 코드
(생략)
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("prod")
class ProductServiceTest {
@Autowired
ProductService productService;
@Transactional // 새로운 상품을 추가하는 행위가 테스트 코드에서 실행되어도 롤백을 통해 DB에는 반영되지 않도록 하는 어노테이션
@Test
@DisplayName("상품을 추가한 후 id로 조회하면 해당 상품이 조회되어야 한다.") // 해당 어노테이션을 통해 테스트의 코드의 이름을 지정
void productAddAndFindById() {
ProductDTO productDTO = new ProductDTO("pencil", 300, 20);
ProductDTO savedProductDTO = productService.add(productDTO);
Long savedProductId = savedProductDTO.getId();
ProductDTO foundProductDTO = productService.findById(savedProductId);
// ProductListRepository에선 '==' 연산기호를 통해 동일성을 비교했다.
// ProductDatabaseRepository에선 'equals()' 메소드를 통해 동등성을 비교한다.
assertTrue(savedProductDTO.getId().equals(foundProductDTO.getId()));
assertTrue(savedProductDTO.getName().equals(foundProductDTO.getName()));
assertTrue(savedProductDTO.getPrice().equals(foundProductDTO.getPrice()));
assertTrue(savedProductDTO.getAmount().equals(foundProductDTO.getAmount()));
}
}
DB를 사용한 테스트 진행시 몇 가지 주의사항이 있습니다.
1. 동일성이 아닌 동등성 비교
→ 생성(UPDATE)에 사용된 인스턴스와 조회(SELECT)에 사용된 인스턴스는 서로 다른 인스턴스
→ '==' 비교 연산자는 인스턴스의 동일성을 비교하므로, equals() 메소드를 통해 값 자체의 동등성을 비교
2. DB에 저장되지 않도록 롤백
→ UPDATE가 수행되면 DB에 값이 추가되므로, 테스트 코드에선 @Transactional을 통해 롤백
→ @Test와 @Transactional을 함께 사용하면 테스트 코드가 실행된 후 롤백을 통해 테스트 이전 상태로 되돌림
예외 상황에 대한 테스트 코드 작성
존재하지 않는 상품 조회 시 EntityNotFoundException이 발생하는지 확인합니다.
ProductListRepository
로컬 개발 환경에서 예외 상황에 대한 테스트를 실시합니다.
소스 코드
(생략)
@SpringBootTest
@ActiveProfiles("test")
class ProductServiceTest {
@Test
@DisplayName("존재하지 앉는 상품 id로 조회하면 EntityNotFoundException이 발생해야 한다.")
void findProductNotExistIdTest() {
Long notExistId = -1L;
// 예외발생을 테스트 하기 위해 assertThrows 사용
// 없는 id(=-1L)를 조회하면 EntityNotFoundException이 발생한다.
assertThrows(EntityNotFoundException.class, () -> {
productService.findById(notExistId);
});
}
}
예외 발생을 테스트하기 위해 assertThrows() 메소드를 사용합니다.
첫 번째 인자로 발생하는 예외 클래스를 지정합니다.
두 번째 인자로는 예외 상황이 발생할 수 있는 코드를 람다 표현식으로 넣어 줍니다.
실행 결과
'Spring-Java > Spring' 카테고리의 다른 글
단위 테스트(@Mock) (0) | 2024.05.30 |
---|---|
인터페이스에 의존하는 Repository (0) | 2024.05.09 |
DB를 사용하는 Repository 구성 2 (0) | 2024.05.08 |
DB를 사용하는 Repository 구성 1 (0) | 2024.05.03 |
전역 예외 핸들러와 예외 처리 전략 (@ControllerAdvice, @ExceptionHandler) (1) | 2024.04.26 |