๐ค Spring Data JPA๋ฅผ ์ฌ์ฉํ๋ฉด์ JPA ๊ด๋ จ ํ ์คํธ๋ง ์งํํ๊ณ ์ถ์ ๋
๋ชจ๋ ๋น์ ๋ฑ๋กํ๋ @SpringBootTest ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ท๋ชจ๊ฐ ์ปค์ง ์๋ก ์๋๊ฐ ๋๋ ค์ง๋๋ค.
ํ์ง๋ง, JPA ๊ด๋ จ ํ ์คํธ๋ง ์งํํ๊ณ ์ถ์ ๊ฒฝ์ฐ๊ฐ ๋ถ๋ช ํ๊ฒ ์กด์ฌํฉ๋๋ค.
์ด์ ๊ฐ์ ๊ฒฝ์ฐ์๋, @DataJpaTest ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํฉ๋๋ค.
@DataJpaTest
class GTMemberEntityTest{
@Autowired
private GTAccountInfoRepository accountInfoRepository;
@Autowired
private GTMemberInfoRepository memberInfoRepository;
@Autowired
private GTAccountUserRoleInfoRepository accountUserRoleInfoRepository;
@Test
@DisplayName("๐ค 1. ๊ณ์ ์ ๋ณด ์์ฑ ๋ฐ ์ ์ฅ ํ
์คํธ : ์ฑ๊ณต ์ผ์ด์ค")
@Transactional
public void testForCreateNewAccountInfoAndSave(){
GTAccountInfo mockAccountInfo = GTAccountInfo.builder()
.accountEmail("test@example.com")
.accountPW("1234")
.build();
GTAccountInfo savedAccountInfo = accountInfoRepository.save(mockAccountInfo);
assertEquals(savedAccountInfo.getAccountEmail(), "test@example.com");
assertEquals(savedAccountInfo.getAccountPW(), "1234");
assertEquals(savedAccountInfo.getRoles().size(), 0);
}
}
์์ ๊ฐ์ด ์์ฑํ๋ฉด, ์๋์ ๊ฐ์ ์ค๋ฅ๋ฅผ ๋ง์ฃผ์ณค์ต๋๋ค.
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.
ํ ์คํธ๋ฅผ ์ํ ์๋ฒ ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ํ DataSource๋ฅผ ๊ต์ฒดํ๋๋ฐ ์คํจํ๋ค๋ ๋ฉ์์ง๋ฅผ ๋ฐ๊ฒฌํ์ต๋๋ค.
์๋ฒ ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ค์ ํ ์ ์ด ์์ง๋ง, ์๋ฒ ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๊ธธ ์ํ๋ค๋ฉด classpath์ ์ ์ฅ๋, ์ง์ํ๋ ๊ฒ์ ์ฌ์ฉํ๊ฑฐ๋ @AutoConfigureTestDatabase ์ด๋ ธํ ์ด์ ์ replace ์ต์ ์ ์กฐ์ ํ๊ธฐ๋ฅผ ๋ฐ๋ผ๊ณ ์์ต๋๋ค.
์ด๋ฌํ ์ค๋ฅ๊ฐ ๋ํ๋ ์ด์ ๋ฅผ ์ดํด๋ณด๊ธฐ ์ํด, @DataJpaTest ์ด๋ ธํ ์ด์ ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
}
๊ธฐ๋ณธ์ ์ผ๋ก, @DataJpaTest์๋ @AutoConfigureTestDatabase๋ผ๋ ์ด๋ ธํ ์ด์ ์ด ๋ฌ๋ ค์์ต๋๋ค.
๊ทธ๋์ ์์ ๊ฐ์ ์๋ฌ๋ฉ์์ง๊ฐ, @DataJpaTest ์ด๋ ธํ ์ด์ ๋ด๋ถ ๋ก์ง์ ์ฒ๋ฆฌํ๋ฉด์ ๋ฐ์ํ๋ค๋ ์ฌ์ค์ ์๊ฒ ๋์์ต๋๋ค.
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
@PropertyMapping("spring.test.database")
public @interface AutoConfigureTestDatabase {
/**
* Determines what type of existing DataSource bean can be replaced.
* @return the type of existing DataSource to replace
*/
@PropertyMapping(skip = SkipPropertyMapping.ON_DEFAULT_VALUE)
Replace replace() default Replace.ANY;
/**
* The type of connection to be established when {@link #replace() replacing} the
* DataSource. By default will attempt to detect the connection based on the
* classpath.
* @return the type of connection to use
*/
EmbeddedDatabaseConnection connection() default EmbeddedDatabaseConnection.NONE;
/**
* What the test database can replace.
*/
enum Replace {
/**
* Replace the DataSource bean whether it was auto-configured or manually defined.
*/
ANY,
/**
* Only replace the DataSource if it was auto-configured.
*/
AUTO_CONFIGURED,
/**
* Don't replace the application default DataSource.
*/
NONE
}
}
๋ด๋ถ๋ฅผ ์ดํด๋ด ์๋ค. ์๋ฌ ๋ฉ์์ง์์ replace attribute๋ฅผ ์กฐ์ ํ๋ผ๊ณ ํ์์ผ๋ฏ๋ก, replace๋ฅผ ๋จผ์ ํ์ธํด๋ด ์๋ค. @AutoConfigureTestDatabase์ replace์ default๊ฐ์ Replace.ANY;์ ํด๋นํฉ๋๋ค.
replace๋ ์๋์ EmbeddedDatabase์ ๊ดํ ์ค์ ์ ๊ฒฐ์ ํฉ๋๋ค.
ANY → ์๋ ๊ตฌ์ฑ๋์๋ , ์๋์ผ๋ก ์ ์ํ๋ ์๊ด์์ด DataSource ๋น์ ๊ต์ฒดํ๋ Enum ๊ฐ
AUTO_CONFIGURED → ์๋ ๊ตฌ์ฑ๋ ๊ฒฝ์ฐ์๋ง DataSource ๋น ๊ต์ฒดํ๋ Enum ๊ฐ
NONE → DataSource๋ฅผ ๊ต์ฒด ํ์ง ์์
๋ฐ๋ผ์, ์ง๊ธ ์ํ๋ก๋ ํ ์คํธ๋ฅผ ์ํ DataSource ๋น์ด ๊ต์ฒด๋๋ค๋ ์ฌ์ค์ ์๊ฒ๋์์ต๋๋ค.
EmbeddedDatabaseConnection์ default ๊ฐ์ผ๋ก EmbeddedDatabaseConnection.NONE;์ ๊ฐ์ต๋๋ค.
EmbeddedDatabaseConnection์, H2, DERBY, HSQLDB ๋ฑ์ ์ง์ํฉ๋๋ค.
public enum EmbeddedDatabaseConnection {
/**
* No Connection.
*/
NONE(null, null, null, (url) -> false),
/**
* H2 Database Connection.
*/
H2(EmbeddedDatabaseType.H2, DatabaseDriver.H2.getDriverClassName(),
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", (url) -> url.contains(":h2:mem")),
/**
* Derby Database Connection.
*/
DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true",
(url) -> true),
/**
* HSQL Database Connection.
* @since 2.4.0
*/
HSQLDB(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
"jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:"));
}
๊ทธ ์ค, default๋ก None์ ๊ฐ์ง๋ฏ๋ก,
private final EmbeddedDatabaseType type;
private final String driverClass;
private final String alternativeDriverClass;
private final String url;
์ ๊ฐ์ ๋ชจ๋ null์ธ Enum๊ฐ์ด ์ ํ๋์๋ค์.
@AutoConfigureTestDatabase์ ์ฃผ์์ ์ดํด๋ณด๋ฉด, TestDatabaseAutoConfiguration์ See Also๊ฐ ๋์ด์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก ํ์ธํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class TestDatabaseAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(prefix = "spring.test.database", name = "replace", havingValue = "ANY",
matchIfMissing = true)
static EmbeddedDataSourceBeanFactoryPostProcessor embeddedDataSourceBeanFactoryPostProcessor() {
return new EmbeddedDataSourceBeanFactoryPostProcessor();
}
}
TestDatabaseAutoConfiguration์ ํ ์คํธ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ํ Auto-configuration์ด๋ผ๊ณ ํฉ๋๋ค.
๊ทธ ์ค, @ConditionalOnProperty ์ด๋ ธํ ์ด์ ์์ replace ๊ฐ์ ๋ํ์ฌ ANY ๊ฐ์ ๊ฐ๋๋ค๋ฉด ์๋ก์ด EmbeddedDataSourceBeanFactoryPostProcessor๋ฅผ ๋์ ํ ๋นํด์ ๋น์ผ๋ก ๋ฑ๋กํ๋ ๋ชจ์ต์ ๋ณผ ์ ์์ต๋๋ค.
EmbeddedDataSourceBeanFactoryPostProcessor๋ BeanDefinitionRegistryPostProcessor๋ฅผ ๊ตฌํํ๊ณ ์๊ณ ,
์ด ์ธํฐํ์ด์ค์ ๋ํ ์ค๋ช ์ ์๋์ ๋งํฌ์์ ํ์ธํ ์ ์์์ต๋๋ค
postProcessBeanDefinitionRegistry๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํด์ BeanFactory๋ฅผ ํตํด์ ๋น ์ ๋ณด๋ฅผ ๋ฑ๋ก์ํฌ ์ ์์ต๋๋ค.
process(registry, (ConfigurableListableBeanFactory) registry);
์์ ๊ฐ์ด ํด๋น ๋ฉ์๋์์ EmbeddedDataSourceBeanFactoryPostProcessor.process๋ฅผ ํธ์ถํ๊ณ ,
registry.registerBeanDefinition(beanName, createEmbeddedBeanDefinition(primary));
process ๋ฉ์๋์์๋ createEmbeddedBeanDefinition์ ํธ์ถํฉ๋๋ค.
createEmbeddedBeanDefinition์์ RootBeanDefinition์ ์ค์ ํ๋ฉด์
EmbeddedDataSourceFactoryBean.class
๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฃ์ต๋๋ค.
EmbeddedDataSourceFactoryBean์ InitializingBean์ ๊ตฌํํ๊ณ ์๊ณ , afterPropertiesSet์ ์ค๋ฒ๋ผ์ด๋ฉ ํจ์ผ๋ก์จ BeanFactory์ ์ํด ๋ชจ๋ property ๊ฐ ์ค์ ๋๊ณ ๋ ๋ค ํด๋น ๋ฉ์๋๊ฐ ์คํ๋ฉ๋๋ค.
ํด๋น ๋ฉ์๋์์๋
@Override
public void afterPropertiesSet() throws Exception {
this.embeddedDatabase = this.factory.getEmbeddedDatabase();
}
์ ๊ฐ์ด getEmbeddedDatabase ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ ์์ต๋๋ค.
๋ง์นจ๋ด, ์ค๋ฅ์์ ๋ฐ๊ฒฌํ ๋ฌธ๊ตฌ๊ฐ ๋ณด์ด๊ธฐ ์์ํฉ๋๋ค.
EmbeddedDatabase getEmbeddedDatabase() {
EmbeddedDatabaseConnection connection = this.environment.getProperty("spring.test.database.connection",
EmbeddedDatabaseConnection.class, EmbeddedDatabaseConnection.NONE);
if (EmbeddedDatabaseConnection.NONE.equals(connection)) {
connection = EmbeddedDatabaseConnection.get(getClass().getClassLoader());
}
Assert.state(connection != EmbeddedDatabaseConnection.NONE,
"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.");
return new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(connection.getType()).build();
}
Assert.state์์ EmbeddedDatabaseConnection.NONE์ด ์๋์ ๋ณด์ฅํ๊ณ ์์ผ๋,
์์์ ๋ดค๋ฏ์ด, EmbeddedDatabaseConnection ๊ฐ์ EmbeddedDatabaseConnection.NONE์ผ๋ก ์ค์ ๋์ด ์์ต๋๋ค. ๋ฐ๋ผ์ ์ด ๋ถ๋ถ์์ ์๋ฌ๊ฐ ๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
๋ฐ๋ผ์ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ , ํ ์คํธ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์๋ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๊ณ ์ ํ๋ค๋ฉด,
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
๊ณผ ๊ฐ์ด replace ๊ฐ์ NONE์ผ๋ก ๋ณ๊ฒฝํด์ฃผ๋ฉด ๋ฉ๋๋ค. ANY๊ฐ Default ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ ๋ฌธ์ ์์ ์ ์ ์์์ต๋๋ค.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class GTMemberEntityTest{
@Autowired
private GTAccountInfoRepository accountInfoRepository;
@Autowired
private GTMemberInfoRepository memberInfoRepository;
@Autowired
private GTAccountUserRoleInfoRepository accountUserRoleInfoRepository;
@Test
@DisplayName("๐ค 1. ๊ณ์ ์ ๋ณด ์์ฑ ๋ฐ ์ ์ฅ ํ
์คํธ : ์ฑ๊ณต ์ผ์ด์ค")
@Transactional
public void testForCreateNewAccountInfoAndSave(){
GTAccountInfo mockAccountInfo = GTAccountInfo.builder()
.accountEmail("test@example.com")
.accountPW("1234")
.build();
GTAccountInfo savedAccountInfo = accountInfoRepository.save(mockAccountInfo);
assertEquals(savedAccountInfo.getAccountEmail(), "test@example.com");
assertEquals(savedAccountInfo.getAccountPW(), "1234");
assertEquals(savedAccountInfo.getRoles().size(), 0);
}
}
Spring Bean์ด ์ด๋ป๊ฒ ๋ฑ๋ก๋๋์ง ์ ํํ ๊ณผ์ ์ ์ ๋ชฐ๋๊ธฐ์ ๋๋ต์ ์ผ๋ก๋ฐ์ ๋ฌธ์ ์ ์์ธ์ ํ์ ํ์ง ๋ชปํ์ต๋๋ค.
๋์ค์ ์ถ๊ฐ์ ์ธ ํ์ต์ ํตํด์ Spring Bean์ด ์ด๋ป๊ฒ ๋ฑ๋ก๋๋์ง ์์ธํ ๊ณต๋ถํ ํ์์ฑ์ ๋๊ผ์ต๋๋ค.