본 포스팅은, 단위 테스트 (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
[리포지토리 단위 테스트]
Repository는 구현되는 레이어 중, 가장 데이터베이스와 가까운 레이어이다.
하지만 데이터베이스를 외부 요인으로 볼 수 있기 때문에, 단위 테스트 시 데이터베이스를 연동하는 부분을 포함시키지 않는 경우도 있다.
하지만, 데이터베이스 연동 부분을 제외한다면 Repository를 테스트 하는 의미가 감소하기 때문에, 연동되는 부분을 테스트하는 방법에 대해 알아보자.
Repository 테스트에서 고려해야하는 내용은 다음과 같다.
- 데이터베이스를 사용하는 테스트는 테스트 과정에서 실제 데이터베이스에 데이터가 적재되므로, 아래와 같은 방법을 통하여 테스트를 진행한다.
- 별도의 테스트용 데이터베이스를 사용한다.
- 테스트로 생성된 데이터를 제거하는 코드까지 포함하여 작성한다.
- 위 내용과 관련하여 다음과 같이 Repository 테스트 환경을 구성하는 부분을 중점적으로 다루겠다.
- Embeded DB 사용 ( H2 DB )
- 실제 개발 환경 DB 사용 ( MariaDB )
- @JpaDataTest 어노테이션 사용
- @SpringBootTest 어노테이션 사용
[테스트 코드]
테스트를 진행할 Repository 는 다음과 같다.
import com.springboot.test.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
}
테스트 진행할 ProductRepository 를 살펴보자.
- Entity
- Product Entity 사용
- JpaRepository 상속
- 별도의 커스텀 메서드 없이, 기본 메서드로 테스트 코드 작성하는 방법에 대해 알아볼 예정이다.
- 실제 개발 환경에서, JpaRepository 에서 제공되는 기본 메서드를 테스트하는 것은 이미 검증이 완료된 메서드기 때문에 무의미한 부분이 있으나, 데이터베이스 연동 부분과 관련된 테스트 환경에 대해 작성하겠다.
<Embeded DB 사용>
우선, Embeded DB인 H2 DB 를 사용하기 위한 의존성 추가부터 진행한다.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
아래는, H2 DB를 사용하는 ProductRepository 테스트 코드이다.
@DataJpaTest
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
//저장 테스트
//테스트에 h2 를 사용한다면, dialect 설정 유의
@Test
public void saveTest() {
//Given
Product product = new Product();
product.setName("연필");
product.setPrice(500000);
product.setStock(2020);
//When
Product savedProduct = productRepository.save(product);
//Then
Assertions.assertEquals(product.getName(), savedProduct.getName());
Assertions.assertEquals(product.getPrice(), savedProduct.getPrice());
Assertions.assertEquals(product.getStock(), savedProduct.getStock());
}
@Test
void selectTest() {
//Given
Product product = new Product();
product.setName("연필");
product.setPrice(500000);
product.setStock(2020);
Product savedProduct = productRepository.saveAndFlush(product);
//When
Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
//Then
Assertions.assertEquals(product.getName(), foundProduct.getName());
Assertions.assertEquals(product.getPrice(), foundProduct.getPrice());
Assertions.assertEquals(product.getStock(), foundProduct.getStock());
}
}
작성된 테스트 코드에 대해 하나씩 살펴보자.
- @DataJpaTest
- JPA 와 관련된 설정만 로드하여 테스트를 진행한다.
- 내부적으로 @Transactional 어노테이션을 포함하고 있어, 테스트 코드가 종료되면 자동으로 DB Rollback 이 진행된다.
위에서 언급한, 생성된 데이터를 제거하는 내용은 해당 어노테이션을 사용함으로써 해결할 수 있다. - Default 로는 Embeded 데이터베이스를 사용하며, 실제 개발 환경에서 사용하는 데이터베이스로 테스트하기 위해서는 별도의 설정을 해주어야 한다.
별도로 설정하는 방법은 다음 예제를 통해 알아보자. - selectTest() 테스트 메서드 중간을 보면, saveAndFlush() 메서드를 사용하고 있다. (36번 라인)
@DataJpaTest 어노테이션을 사용한 테스트이므로, "product" 테이블은 비어있는 상태이며, saveAndFlush() 메서드를 통해 조회해올 데이터를 미리 테이블에 넣어놓는다. - saveAndFlush() 메서드는 Entity를 저장하고, 영속성 컨텍스트의 변경사항을 데이터베이스에 즉각 반영한다.
그러나, 트랜잭션을 커밋하는 것은 아니며, 롤백이 발생하는 경우 해당 데이터도 롤백된다.
즉, 조회해오기 위해 테이블에 임시(?)적으로 적재시키는 메서드이다.
- Given - When - Then 패턴
- selectTest() 메서드로 설명하겠다.
- Given
- 30번 라인
findById() 메서드를 통해 조회할 Product Entity 를 정의한다.
- 30번 라인
- When
- 38번 라인
findById() 메서드 테스트 진행
- 38번 라인
- Then
- 41번 라인
- When 단계에서 리턴받은 Product Entity 객체 (foundProduct) 에 대한 값을 검증한다.
Jupiter Assertions 을 사용하여 리턴된 결과를 검증
- DB dialect 설정
- dialect 는 JPA와 데이터베이스 간의 일관성을 유지하기 위해 사용하는 설정이다.
만약, dialect 를 실제 사용하는 DB로 설정하였다면 오류가 발생한다.
개발 환경은 MariaDB 로 구성되어 있으나, H2 를 사용하려 하니, 문법이 맞지 않아 발생하는 오류이며, 이를 해결하기 위해 테스트용 application.properties 를 따로 구성하기도 한다.
=오류 예시=
<application-test.properties 파일로 테스트 환경 구축하기>
위에서 언급한, 테스트용 application.properties 를 구성해보자.
- test 디렉토리 내, application-test.properties 파일을 생성한다.
application-test.properties 생성 - 테스트 시, 사용할 애플리케이션 설정 정보를 작성한다.
# H2 DB 설정 spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
- Test 코드에 @ActiveProfilies 어노테이션을 활용해 application-test.properties 파일을 바라보도록 추가
@DataJpaTest @ActiveProfiles("test") //application-test.properties 를 사용하도록 설정 public class ProductRepositoryTestByH2 { @Autowired private ProductRepository productRepository; ... 이하 생략
- 실행 결과
selectTest() 실행 결과
테스트를 실행한 로그를 자세히 살펴보자.
이와 같은 절차로 ProductRepository 가 테스트 된 것을 확인할 수 있다.
<Persistence Context 확인하기>
근데, 이상한 부분이 있다.
테이블을 생성하는 CREATE 쿼리와, saveAndFlush() 에서 실행되는 INSERT 쿼리는 모두 로그에 기재되었으나, findById() 메서드에 대한 로그가 찍히지 않은 것이다.
그렇다. saveAndFlush() 로 생성된 데이터가 Persistence Context (영속성 컨텍스트) 의 캐시에 저장되어 있기 때문에, DB를 조회하지 않는 것이다.
** 그렇다면 EntityManager 를 사용하여, 영속성 컨텍스트에 savedProduct 엔티티가 저장되어 있는지 확인해보자.
EntityManager 의존성을 주입하고,
Assertion 을 사하여, Persistence Context에 savedProduct 가 존재하는 경우 에러가 발생하게 하였다.
@DataJpaTest
@ActiveProfiles("test")
public class ProductRepositoryTestByH2 {
@Autowired
private EntityManager entityManager; //EntityManager 의존성 주입
@Test
void selectTest() {
//Given
Product product = new Product();
product.setName("연필");
product.setPrice(500000);
product.setStock(2020);
Product savedProduct = productRepository.saveAndFlush(product);
//When
Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
//Then
//Assertion을 사용하여 Persistence Context에 savedProduct가 있는 경우 에러 발생하도록 작성
Assertions.assertFalse(entityManager.contains(savedProduct), "Saved Product is in the Persistence Context");
//Assertions.assertEquals(product.getName(), foundProduct.getName());
//Assertions.assertEquals(product.getPrice(), foundProduct.getPrice());
//Assertions.assertEquals(product.getStock(), foundProduct.getStock());
}
}
테스트 실행 결과, 위와 같이 savedProduct 가 영속성 컨텍스트에 존재하기 때문에 오류가 발생하는 것을 확인할 수 있다.
만약 로그로 select 되는 내용을 보고 싶다면, findById() 메서드를 호출하기 전에 entityManager 를 통해 영속성 컨텍스트를 클리어 해주면 된다.
... 생략 ...
Product savedProduct = productRepository.saveAndFlush(product);
//When
entityManager.clear();
Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
//Then
Assertions.assertFalse(entityManager.contains(savedProduct), "Saved Product is in the Persistence Context");
... 생략 ...
이처럼, 똑같은 Assert 구문이 작성되어 있으나, entityManager.clear() 를 통해 영속성 컨텍스트 캐시를 비웠기 때문에 findById() 에서 사용하는 SELECT 쿼리가 로그에 기재되는 것을 확인할 수 있다.
<실제 개발환경과 동일한 데이터베이스 사용>
이번엔, 실제 개발환경과 동일한 환경에서 테스트 할 수 있도록 구성해보자.
- @AutoConfigureTestDatabase 어노테이션 사용
- @AutoConfigureTestDatabase는 Spring Boot의 통합 테스트에서 데이터베이스 설정을 자동으로 구성할 때 사용된다.
앞서 설명했다시피, 별다른 설정이 없다면 빠르고 독립적으로 실행될 수 있도록 하기 위해 실제 데이터베이스를 사용하지 않고 임베디드 데이터베이스(H2, HSQL, Derby 등)를 사용한다. - 이 어노테이션의 replace 속성으로 테스트 중에 사용할 데이터베이스를 어떻게 대체할 것인지 설정할 수 있다.
- Replace.ANY (Default)
- Embeded 데이터베이스가 사용 가능하면, 애플리케이션의 실제 데이터베이스 설정을 임베디드 데이터베이스로 대체한다.
- 앞서 작성한 예제에서는 H2 DB를 사용할 예정이였으므로, 해당 어노테이션을 따로 작성하지 않고 Default 옵션으로 사용한 것이다.
- Replace.NONE
- Spring Boot가 자동으로 데이터베이스 설정을 임베디드 데이터베이스로 대체하지 않도록 한다.
- 이 설정을 사용하면 애플리케이션의 실제 데이터베이스 설정이 그대로 사용된다.
- Replace.ANY (Default)
- Source
@DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class ProductRepositoryTestWithProjectConfig { ... 이하 생략 }
- @AutoConfigureTestDatabase는 Spring Boot의 통합 테스트에서 데이터베이스 설정을 자동으로 구성할 때 사용된다.
- @SpringBootTest 어노테이션 사용
- 주로 Spring Boot에서 통합 테스트를 수행할 때 사용되는 어노테이션으로, 실제 애플리케이션 컨텍스트를 로드하고 모든 빈을 초기화하여 전체 애플리케이션을 테스트할 수 있다.
- 다만, 모든 스프링 설정을 가져오고, 모든 빈 객체를 스캔하기 때문에 단위 테스트에서는 적합하지 않을 수 있다.
- Source
@SpringBootTest public class ProductRepositoryTestWithSpringBootAnno { ...이하 생략 }
8개의 포스팅에 걸쳐, Spring Boot 환경에서 Test 에 대해 알고 있으면 좋은 개념 / Framework / Library 등에 대해 알아봤으며, Controller - Service - Repository 구조에서 각각의 레이어에 대해 단위 테스트를 진행하는 방법을 작성하였다.
2024.08.04 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 1 - 테스트 개요
2024.08.05 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 2 - TDD 테스트 주도 개발이란?
2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 3 - JUnit5
2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 4 - Assertion 단정문
2024.08.06 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 5 - Mockito
2024.08.08 - [Programming/Spring & Spring boot] - [Springboot] 테스트 코드 - 8 - Repository Unit Test 리포지토리 단위 테스트
[GitHub]
[참고 도서]

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