본문 바로가기
스프링

[스프링] JPA 관련 통합 테스트 환경 구축하기

by kdohyeon (김대니) 2023. 2. 6.
반응형

통합 테스트 환경을 구축하는 법을 알아보자.

테스트 대상

통합 테스트 환경을 구축하면서 테스트를 해볼 대상은 영속성 (Persistence) 관련 클래스들이다. JPA 를 사용해서 데이터에 대한 생성, 조회, 수정, 삭제, 즉 CRUD 기능을 테스트해보려고 합니다. 테스트를 위한 DB 는 h2 를 사용합니다.

예시에서 사용될 테스트 대상으로 JpaRepository 를 상속받는 BlogStatisticJpaRepository

public interface BlogStatisticJpaRepository extends JpaRepository<BlogStatistic, Long>, BlogStatisticCustomRepository {

}

의존성 설정

아래 의존성을 build.gradle 파일에 적용한다.

// build.gradle.kts
plugins {
    id("com.coditory.integration-test") version "1.4.5" apply false
}

dependencies {
    integrationImplementation("org.junit.jupiter:junit-jupiter-api")
    integrationImplementation("org.junit.jupiter:junit-jupiter-params")
    integrationImplementation("org.assertj:assertj-core")
    integrationRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
    
    // 테스트 관련 설정을 위한 라이브러리
    integrationImplementation("org.springframework.boot:spring-boot-starter-test")

    // 테스트 데이터 환경 구축을 위한 h2 DB
    integrationRuntimeOnly("com.h2database:h2")
}

여기까지 추가하고 gradle sync 를 한다.

활용한 외부 라이브러리

아래 라이브러리를 활용하면 소스코드 내 "integration" 디렉토리를 IDE 에서 자동으로 통합 테스트를 작성할 수 있는 디렉토리로 인식하게 해준다.

 

GitHub - coditory/gradle-integration-test-plugin: Gradle plugin with integrationTest task

Gradle plugin with integrationTest task. Contribute to coditory/gradle-integration-test-plugin development by creating an account on GitHub.

github.com

디렉토리 만들기

:libs:adapter-persistence 모듈은 DB 관련 코드가 포함되어 있는 영속성 모듈이다. 영속성 모듈에 통합 테스트를 만들기 위해 src 아래 새로운 디렉토리를 생성한다.

:libs:adapter-persistence 모듈의 src 디렉토리 오른쪽 마우스 클릭 > New > Directory 클릭

New > Directory 클릭

이제 통합 테스트를 작성할 수 있는 새로운 디렉토리 integration/java, integration/resources 를 생성할 수 있다.

이런 화면이 나오지 않으면 "coditory.integration-test" 의존성이 제대로 주입 안된 것

설정 (Config.) 파일 만들기

IntegrationConfig.java 와 IntegrationTest.java 파일을 만든다. 

  • IntegrationConfig.java - 스프링 빈을 사용할 수 있게 @ComponentScan 을 한다.
  • IntegrationTest.java - 각 통합 테스트의 base 환경을 제공한다.

폴더 구조는 이렇게 만들어지고 있다.

 

IntegrationConfig.java

@Configuration
@ComponentScan(basePackages = "sample.kdohyeon.blog")
public class IntegrationConfig {
}

 

IntegrationTest.java

  • @DataJpaTest 어노테이션 - @Entity 어노테이션이 적용된 클래스를 스캔하며 @Repository 어노테이션이 부여된 클래스들도 포함시킨다.
  • @ContextConfiguration - ApplicationContext 관련 속성은 PersistenceJpaConfig 를 참조한다.
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
@ComponentScan(basePackageClasses = {PersistenceModule.class})
@ContextConfiguration(classes = {PersistenceJpaConfig.class})
public abstract class IntegrationTest {
}

 

테스트 작성하기

public class BlogStatisticJpaRepositoryTest extends IntegrationTest {
    @Autowired
    BlogStatisticJpaRepository blogStatisticJpaRepository;

    @Test
    void save() {
        var blogStat = BlogStatistic.create(
                CreateBlogStatistic.builder()
                        .keyword("keyword")
                        .build()
        );

        blogStatisticJpaRepository.save(blogStat);

        assertThat(blogStat).isNotNull();
        assertThat(blogStat.getId()).isNotNull();
        assertThat(blogStat.getCount()).isEqualTo(1L);
        assertThat(blogStat.getKeyword()).isEqualTo("keyword");
    }
}

테스트 결과

  • 첫 번째 빨간 박스: @Entity 에 기반해서 테스트 테이블을 만든다
  • 두 번째 빨간 박스: 테스트 코드에서 .save(...) 를 하기 때문에 새로운 row 를 insert 한다
  • 세 번째 빨간 박스: 테스트 테이블을 제거한다

> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources NO-SOURCE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:inspectClassesForKotlinIC UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :buildSrc:assemble UP-TO-DATE
> Task :buildSrc:compileTestKotlin NO-SOURCE
> Task :buildSrc:pluginUnderTestMetadata UP-TO-DATE
> Task :buildSrc:compileTestJava NO-SOURCE
> Task :buildSrc:compileTestGroovy NO-SOURCE
> Task :buildSrc:processTestResources NO-SOURCE
> Task :buildSrc:testClasses UP-TO-DATE
> Task :buildSrc:test NO-SOURCE
> Task :buildSrc:validatePlugins UP-TO-DATE
> Task :buildSrc:check UP-TO-DATE
> Task :buildSrc:build UP-TO-DATE
> Task :libs:adapter-persistence:generateEffectiveLombokConfig
> Task :libs:application:generateEffectiveLombokConfig
> Task :libs:application:compileJava UP-TO-DATE
> Task :libs:adapter-persistence:compileJava UP-TO-DATE
> Task :libs:adapter-persistence:processResources UP-TO-DATE
> Task :libs:adapter-persistence:classes UP-TO-DATE
> Task :libs:adapter-persistence:generateIntegrationEffectiveLombokConfig
> Task :libs:adapter-persistence:generateTestEffectiveLombokConfig
> Task :libs:adapter-persistence:compileTestJava NO-SOURCE
> Task :libs:adapter-persistence:processTestResources NO-SOURCE
> Task :libs:adapter-persistence:testClasses UP-TO-DATE
> Task :libs:adapter-persistence:compileIntegrationJava
> Task :libs:adapter-persistence:processIntegrationResources NO-SOURCE
> Task :libs:adapter-persistence:integrationClasses
> Task :libs:application:processResources NO-SOURCE
> Task :libs:application:classes UP-TO-DATE
> Task :libs:application:jar UP-TO-DATE
> Task :libs:adapter-persistence:integrationTest
09:27:50.376 [Test worker] INFO  o.s.t.c.s.AbstractContextLoader - Could not detect default resource locations for test class [sample.kdohyeon.blog.IntegrationTest]: no resource found for suffixes {-context.xml, Context.groovy}.
09:27:50.532 [Test worker] INFO  o.s.b.t.a.o.j.DataJpaTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.event.ApplicationEventsTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
09:27:50.544 [Test worker] INFO  o.s.b.t.a.o.j.DataJpaTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@4b85880b, org.springframework.test.context.event.ApplicationEventsTestExecutionListener@4f936da8, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@4215838f, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@452ba1db, org.springframework.test.context.support.DirtiesContextTestExecutionListener@2289aca5, org.springframework.test.context.transaction.TransactionalTestExecutionListener@76a36b71, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@184497d1, org.springframework.test.context.event.EventPublishingTestExecutionListener@f9d87b, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@6ffab045, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@26fb628, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@3e2943ab, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@70dd7e15, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@4a9f80d3, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@35beb15e]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.3)

09:27:51.046 [background-preinit] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 6.2.4.Final
09:27:51.073 [Test worker] INFO  s.k.b.p.s.BlogStatisticJpaRepositoryTest - Starting BlogStatisticJpaRepositoryTest using Java 17.0.2 on ip-192-168-0-34.ap-northeast-2.compute.internal with PID 80632 (started by trenbe in /Users/trenbe/IdeaProjects/blog-service/libs/adapter-persistence)
09:27:51.076 [Test worker] INFO  s.k.b.p.s.BlogStatisticJpaRepositoryTest - No active profile set, falling back to 1 default profile: "default"
09:27:51.734 [Test worker] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
09:27:51.811 [Test worker] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 72 ms. Found 1 JPA repository interfaces.
09:27:51.961 [Test worker] INFO  o.s.b.t.a.j.TestDatabaseAutoConfiguration$EmbeddedDataSourceBeanFactoryPostProcessor - Replacing 'dataSource' DataSource bean with embedded version
09:27:52.247 [Test worker] INFO  o.s.j.d.e.EmbeddedDatabaseFactory - Starting embedded database: url='jdbc:h2:mem:999a12d6-39f1-4db8-8dea-9a0b37b251b7;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
09:27:52.695 [Test worker] INFO  o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
09:27:52.727 [Test worker] INFO  org.hibernate.Version - HHH000412: Hibernate ORM core version 5.6.10.Final
09:27:52.915 [Test worker] INFO  o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
09:27:53.007 [Test worker] INFO  org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop table if exists blog_statistic CASCADE 
Hibernate: create table blog_statistic (id bigint generated by default as identity, count bigint not null, keyword varchar(255) not null, primary key (id))
09:27:53.454 [Test worker] INFO  o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
09:27:53.459 [Test worker] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'default'
09:27:54.174 [Test worker] INFO  s.k.b.p.s.BlogStatisticJpaRepositoryTest - Started BlogStatisticJpaRepositoryTest in 3.591 seconds (JVM running for 5.215)
09:27:54.229 [Test worker] INFO  o.s.t.c.t.TransactionContext - Began transaction (1) for test context [DefaultTestContext@3bf26810 testClass = BlogStatisticJpaRepositoryTest, testInstance = sample.kdohyeon.blog.persistence.statistics.BlogStatisticJpaRepositoryTest@19213a74, testMethod = save@BlogStatisticJpaRepositoryTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@30a791a6 testClass = BlogStatisticJpaRepositoryTest, locations = '{}', classes = '{class sample.kdohyeon.blog.configure.PersistenceJpaConfig}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@662f5666, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@502f1f4c, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@5515df6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@6cbc4f2b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@618c5d94, [ImportsContextCustomizer@719bb60d key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@33e01298, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4b3c354a, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@3beaa16d]; rollback [true]
Hibernate: insert into blog_statistic (id, count, keyword) values (default, ?, ?)
09:27:54.556 [Test worker] INFO  o.s.t.c.t.TransactionContext - Rolled back transaction for test: [DefaultTestContext@3bf26810 testClass = BlogStatisticJpaRepositoryTest, testInstance = sample.kdohyeon.blog.persistence.statistics.BlogStatisticJpaRepositoryTest@19213a74, testMethod = save@BlogStatisticJpaRepositoryTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@30a791a6 testClass = BlogStatisticJpaRepositoryTest, locations = '{}', classes = '{class sample.kdohyeon.blog.configure.PersistenceJpaConfig}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@662f5666, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@502f1f4c, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@5515df6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@6cbc4f2b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@618c5d94, [ImportsContextCustomizer@719bb60d key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@33e01298, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4b3c354a, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]
09:27:54.593 [SpringApplicationShutdownHook] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
09:27:54.593 [SpringApplicationShutdownHook] INFO  o.h.t.s.i.SchemaDropperImpl$DelayedDropActionImpl - HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
Hibernate: drop table if exists blog_statistic CASCADE 
BUILD SUCCESSFUL in 10s
16 actionable tasks: 6 executed, 10 up-to-date
9:27:54 AM: Execution finished ':libs:adapter-persistence:integrationTest --tests "sample.kdohyeon.blog.persistence.statistics.BlogStatisticJpaRepositoryTest"'.

 

참고 자료

https://webcoding-start.tistory.com/20

 

스프링 부트 테스트 : @DataJpaTest

@DataJpaTest @DataJpaTest 어노테이션은 JPA 관련 테스트 설정만 로드합니다. DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능합니다. 그리고

webcoding-start.tistory.com

https://galid1.tistory.com/735

 

Spring Boot - 스프링 부트 통합테스트 방법과 팁(Spring boot Integration Test)

Spring Boot 테스트 이번 포스팅에서는 Spring Boot에서 통합테스트하는 방법에 대해서 알아보려고 합니다. https://medium.com/@ssowonny/%EC%84%A4%EB%A7%88-%EC%95%84%EC%A7%81%EB%8F%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%9

galid1.tistory.com

반응형

댓글