Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

DevYGwan

DB관련 Test 코드 작성 시 유의할 점 :: @DataJpaTest 본문

Study/Spring

DB관련 Test 코드 작성 시 유의할 점 :: @DataJpaTest

YGwan 2024. 5. 4. 15:40

요즘 들어, 테스트 코드에 관심히 생겨 테스트 코드를 공부하고 있습니다. 예전에는 단순히 내가 일일이 기능을 사용해보면서 제대로 기능이 수행되는지 확인하는 식의 수동 테스트로 테스트 코드를 별도로 짜지 않고 기능 구현 위주로 코드를 작성했습니다. 단순히 "내가 직접 사용해보면서 테스트해보면 되겠지" 라는 안일한 생각을 가지고 주어진 테스크를 만족하기 위한 기능 구현 위주의 개발을 했었습니다. 그러다보니 계속해서 문제가 발생하더라구요... 그래서 이번 기회에 테스트 코드를 제대로 짜보자 라는 생각이 들었습니다. 더이상 미루면 안될 것 같더라구요... ㅎㅎ

 


※  테스트 코드란

 간단하게 말해서 소프트웨어의 기능과 동작을 테스트하는데 사용되는 코드로, 작은 코드 단위(클래스 & 메서드)를 독립적으로 검증하는 테스트인 단위 테스트(Unit Test)와 단위 테스트에서 검증된 개별 기능들을 결합하여 예상대로 상호작용 하고 있는지를 확인하는 테스트인 통합 테스트(Integration Test)로 나뉩니다. 그렇다면 이러한 테스트 코드가 중요한 이유가 무엇일까요?

 

※  테스트 코드가 중요한 이유

 저는 여태까지 제가 구현한 기능을 일일이 실행해보면서 테스트를 진행했습니다. Postman을 통해 api 테스트를 진행하든, 프론트가 생기면 프론트와 같이 사용해보면서 테스트를 진행하는 식으로요. 그러다보니 유연하게 테스트를 하기가 힘들다는 생각이 들었습니다. 코드가 바뀌면 일일이 테스트를 다시 처음부터 해야된다거나, 내가 어떤 테스트를 진행했었는지 관리가 힘들다거나, 항상 테스트를 진행할때마다 재배포를 하고 반영된 상태에서 테스트를 진행해야 된다거나 등등의 문제가 생겼습니다. 하지만 가장 큰 문제는 테스트를 하는데 시간이 너무 많이 들고 꼼꼼하게 테스트를 하기가 힘들기 때문에 이후 서비스 하는 과정에서 문제가 발생하기 쉽다는 문제가 있습니다. 따라서 이를 코드 단에서 관리한다면 더 빠르고 확실하게 테스트가 가능하기에 테스트 코드를 작성하는 것이 중요합니다.

물론 이렇게 테스트 코드를 통해 테스트를 자동화 하더라도 QA(Quality Assurance) 같은 사람이 수동으로 테스트를 진행하는 과정 또한 중요합니다.

 

그래서 계속 미뤄왔던 테스트 코드를 공부하던 도중, DB 테스트 과정에서 생긴 문제를 공유하려고 합니다. 단순한 DB에 데이터를 저장하는 테스트였습니다. 테스트 환경은 JUnit5, MySQL을 가지고 테스트를 진행했습니다.

 


※  코드 예시

@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @DisplayName("제품 DB에 데이터를 저장하고 조회해보는 테스트")
    @Test
    void test() {
        // DB insert test 진행
    }
}

 

단순히 DB에 데이터를 저장하고 제대로 조회하는 간단한 CR 테스트입니다. 이 테스트를 진행하는 과정에서 에러가 발생했습니다. 에러 내용은 아래와 같습니다.

 

※  에러 내용

java.lang.IllegalStateException: Failed to load ApplicationContext

	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)
	at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:118)
	at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83)
	at org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener.prepareTestInstance(SpringBootDependencyInjectionTestExecutionListener.java:43)
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:248)
	at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:138)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$8(ClassBasedTestDescriptor.java:363)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:368)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$9(ClassBasedTestDescriptor.java:363)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
	at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
	at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:362)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$6(ClassBasedTestDescriptor.java:283)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:282)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$4(ClassBasedTestDescriptor.java:272)
	at java.base/java.util.Optional.orElseGet(Optional.java:369)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$5(ClassBasedTestDescriptor.java:271)
	at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:102)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:101)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Unsatisfied dependency expressed through method 'dataSourceScriptDatabaseInitializer' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of @AutoConfigureTestDatabase.
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:541)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1154)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:908)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:132)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
	... 72 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of @AutoConfigureTestDatabase.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
	... 92 more
Caused by: java.lang.IllegalStateException: Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of @AutoConfigureTestDatabase.
	at org.springframework.util.Assert.state(Assert.java:76)
	at org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration$EmbeddedDataSourceFactory.getEmbeddedDatabase(TestDatabaseAutoConfiguration.java:190)
	at org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration$EmbeddedDataSourceFactoryBean.afterPropertiesSet(TestDatabaseAutoConfiguration.java:149)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
	... 103 more

 

이렇게 보면 에러가 너무 복잡하기에 간단히 요약하자면, 메인 내용은

 

Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of @AutoConfigureTestDatabase.

 

입니다. 해석해보자면, 테스트를 위해 DataSource를 임베디드 데이터베이스로 교체하지 못했습니다. 임베디드 데이터베이스를 원할 경우 클래스 경로에 지원되는 데이터베이스를 넣거나 @AutoConfigureTestDatabase의 교체 속성을 조정하십시오. 

 

왜 이런 에러가 발생하는 것일까요? 이는 @DataJpaTest 어노테이션의 속성 때문입니다.

 

※  @DataJpaTest

  • 스프링 부트 프레임워크에서 제공하는 테스트용 어노테이션 중 하나이다.
  • JPA 관련 기능을 테스트할 때 주로 사용하며, 보통 JPA Repository, Entity 등을 테스트하는 데에 사용된다.
  • 기본적으로 @DataJpaTest로 주석이 달린 테스트는 트랜잭션 방식으로 동작하며, 각 테스트가 끝나면 자동으로 롤백된다.
  • 임베디드 인메모리 데이터베이스를 사용한다. (명시적이거나 일반적으로 자동으로 구성되는 DataSource를 대체한다.)
  • 임베디드 인메모리 데이터베이스의 경우 @AutoConfigureTestDatabase 주석을 사용하여 이러한 설정을 재정의할 수 있다.
  • 전체 애플리케이션 구성을 로드하려고 하지만 임베디드 데이터베이스를 사용하는 경우 이 주석보다는 @AutoConfigureTestDatabase와 결합된 @SpringBootTest를 고려해야한다.

 

이에 대한 더 자세한 내용은 아래의 spring 공식 홈페이지에서 더 자세히 확인 할 수 있습니다.

https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.html

 

DataJpaTest (Spring Boot 3.2.5 API)

A set of exclude filters which can be used to filter beans that would otherwise be added to the application context.

docs.spring.io

 

저는 여기서, 

임베디드 인메모리 데이터베이스를 사용한다.

전체 애플리케이션 구성을 로드하려고 하지만 임베디드 데이터베이스를 사용하는 경우 이 주석보다는 @AutoConfigureTestDatabase와 결합된 @SpringBootTest를 고려해야한다.

 

이 문장에 집중했습니다. 더 자세히 살펴보니, @DataJpaTest 어노테이션이 붙은 테스트 클래스는 인메모리 데이터베이스(H2, HSQL, 또는 Derby)를 사용하기 때문에 실제 데이터베이스가 아닌 메모리에 데이터베이스를 생성하여 테스트를 수행하기 때문에 테스트 속도가 빠르고 테스트 실행 시 부하를 줄일 수 있다는 장점이 존재했습니다. 즉, 기본적으로 @DataJpaTest는 실제 물리적인 데이터베이스가 아닌 인메모리 데이터베이스를 디폴트로 사용하기 때문에 테스트 환경에서 물리적인 메모리(MySQL)를 사용할 경우 별도의 처리가 없으면 문제가 생기는 것이였습니다. 이를 코드로 좀 더 자세히 살펴보도록 하겠습니다.

 

※ @DataJpaTest

@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Documented
@java.lang.annotation.Inherited
@org.springframework.test.context.BootstrapWith(org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper.class)
@org.junit.jupiter.api.extension.ExtendWith({org.springframework.test.context.junit.jupiter.SpringExtension.class})
@org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration(enabled = false)
@org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters({org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTypeExcludeFilter.class})
@org.springframework.transaction.annotation.Transactional
@org.springframework.boot.test.autoconfigure.core.AutoConfigureCache
@org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa
@org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
@org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager
@org.springframework.boot.autoconfigure.ImportAutoConfiguration
public @interface DataJpaTest {
	...
}
  • 보시는 바와 같이 기본적으로 @Transactional이 존재하는 것을 확인할 수 있습니다.
  • 그리고 위에서 보는 @AutoConfigureTestDatabase이 존재하는 것을 확인할 수 있습니다.

 

그렇다면 @AutoConfigureTestDatabase를 좀 더 자세히 살펴보도록 하겠습니다.

 

※ @AutoConfigureTestDatabase

@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Documented
@java.lang.annotation.Inherited
@org.springframework.boot.autoconfigure.ImportAutoConfiguration
@org.springframework.boot.test.autoconfigure.properties.PropertyMapping("spring.test.database")
public @interface AutoConfigureTestDatabase {
    @org.springframework.boot.test.autoconfigure.properties.PropertyMapping(skip = org.springframework.boot.test.autoconfigure.properties.SkipPropertyMapping.ON_DEFAULT_VALUE)
    org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace replace() default org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.ANY;

    org.springframework.boot.jdbc.EmbeddedDatabaseConnection connection() default org.springframework.boot.jdbc.EmbeddedDatabaseConnection.NONE;

    static enum Replace {
        ANY, AUTO_CONFIGURED, NONE;

        private Replace() { /* compiled code */ }
    }
}

 

여기에서 주목해야 할 문장은 

  • org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace replace() default org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.ANY
  • org.springframework.boot.jdbc.EmbeddedDatabaseConnection connection() default org.springframework.boot.jdbc.EmbeddedDatabaseConnection.NONE

입니다.

 

※  AutoConfigureTestDatabase.Replace.ANY

데이터베이스 설정에서 Replace 속성을 정의합니다. 이 속성은 테스트 환경에서 데이터베이스를 교체할 때의 동작을 설정합니다. 기본값은 어떤 데이터베이스도 교체 될 수 있는 ANY입니다.

spring.io 공식문서

Replace의 Enum 값은 크게 3가지 입니다.

  • ANY : 자동 구성되었거나 수동으로 정의되었는지 여부에 관계없이 DataSource 대체
  • AUTO_CONFIGURED : 자동으로 구성된 경우에만 DataSource 교체
  • NONE : 애플리케이션 기본 DataSource 변경 X

 

※  EmbeddedDatabaseConnection.NONE

이 부분은 테스트 환경에서 사용할 데이터베이스 연결을 정의합니다. 기본값으로는 어떠한 테스트 데이터베이스에 대한 연결을 사용하지 않는 NONE입니다.

EmbeddedDatabaseConnection의 Enum 값은 크게 3가지 입니다.

  • DERBY : Derby 데이터베이스 연결
  • H2 : H2 데이터베이스 연결
  • HSQLDB : HSQLDB 데이터베이스 연결
  • NONE : 연결 X

 

즉, 테스트 환경에서 임베디드 할 수 있는 DB의 경우 DERBY, H2, HSQLDB이 있는 것을 확인할 수 있습니다.

 

따라서 저의 경우 MySQL을 테스트 환경에서의 DB로 사용했기 때문에 발생한 문제였습니다. 기본적으로 Test 환경에서는 임베디드 인메모리 데이터베이스를 사용하는데, MySQL로 사용할 수 없기 때문입니다.

 


※  해결방법

그렇다면, 위와 같은 문제를 해결하기 위한 방법이 어떤게 있는지 확인해보도록 하겠습니다.

 

 * 해결방법 1 : 코드 단에서 @AutoConfigureTestDatabase replace 기본값 변경하기

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @DisplayName("제품 DB에 데이터를 저장하고 조회해보는 테스트")
    @Test
    void test() {
        // DB insert test 진행
    }
}
  • replace 전략을 NONE으로 변경하면, 테스트 실행 중에 Spring Boot는 프로덕션 환경과 동일한 데이터베이스를 사용하게 됩니다. 따라서 MySQL을 사용하더라도 문제가 발생하지 않습니다.

 

 * 해결방법 2 : properties 단에서 @AutoConfigureTestDatabase replace 기본값 변경하기

spring.test.database.replace=none
  • 이것 또한 replace 전략을 NONE으로 변경합니다. 하지만 properties파일로 설정할 수 있기 때문에 test코드는 변경되지 않는다는 장점이 있습니다.

 

 * 해결방법 3 : test 환경 DB를 h2 데이터베이스로 변경

implementation 'com.h2database:h2'
testImplementation 'com.h2database:h2'
  • h2 의존성을 추가한다면, 자동으로 테스트 환경에서 h2를 사용해 동작합니다.
  • 별로의 properties파일 없이 동작하는 것을 확인했습니다.

 

 * 해결방법 4 : @DataJpaTest 대신 @SpringBootTest 사용

@SpringBootTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @DisplayName("제품 DB에 데이터를 저장하고 조회해보는 테스트")
    @Test
    void test() {
        // DB insert test 진행
    }
}
  • @SpringBootTest를 주석으로 지정하면 Spring Boot에게 전체 애플리케이션 컨텍스트를 로드하도록 알려준다.
  • 애플리케이션이 실행될 때 일반적으로 로드되는 모든 빈, 구성 및 컴포넌트를 로드한다는 것을 의미한다.
  • 따라서 @SpringBootTest의 경우 실제 DB 자체를 사용하는 테스트를 진행하기 때문에 문제가 발생하지 않는다.

 

3번째 방법(h2 db 사용)이 @DataJpaTest의 이점을 살릴 수 있는 방법이라고 생각하기 때문에 3번째 방법이 가장 좋은 방법이라고 생각합니다.

 


※  정리

@DataJpaTest의 경우 임베디드 인메모리 데이터베이스를 사용합니다. 사용할 수 있는 인메모리 데이터베이스 종류로는 DERBY, H2, HSQLDB가 있습니다. 만약, 다른 데이터베이스를 사용할 경우(임베디드 데이터베이스를 사용하는 경우) 이 주석보다는 @SpringBootTest를 고려해야합니다. 개인적으로는 실제 환경은 임베디드 데이터베이스(MySQL, postgresql, Mariadb 등)을 사용하고 테스트 환경에서는 h2와 같은 데이터베이스를 사용하는 것이 실제 환경에서는 더 안정성을, 테스트 환경에서는 더 빠른 결과값을 줄 수 있는 방법이라고 생각합니다.

 


출처