I am testing TestContainers and I would like to know how to populate a database executing a .sql file to create the structure and add some rows.
How to do it?
#Rule
public PostgreSQLContainer postgres = new PostgreSQLContainer();
The easiest way is to use JdbcDatabaseContainer::withInitScript
Advantage of this solution is that script is run before Spring Application Context loads (at least when it is in a static block) and the code is quite simple.
Example:
static {
postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.8")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa");
postgreSQLContainer
.withInitScript("some/location/on/classpath/someScript.sql");
postgreSQLContainer.start();
}
JdbcDatabaseContainer is superclass of PostgreSQLContainer so this solution should work not only for postgres, but also for other containers.
If you want to run multiple scripts you can do it in a similar manner
Example:
static {
postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.8")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa");
postgreSQLContainer.start();
var containerDelegate = new JdbcDatabaseDelegate(postgreSQLContainer, "");
ScriptUtils.runInitScript(containerDelegate, "some/location/on/classpath/someScriptFirst.sql");
ScriptUtils.runInitScript(containerDelegate, "some/location/on/classpath/someScriptSecond.sql");
ScriptUtils.runInitScript(containerDelegate, "ssome/location/on/classpath/someScriptThird.sql");
}
There are also other options
Spring Test #Sql annotation
#SpringBootTest
#Sql(scripts = ["some/location/on/classpath/someScriptFirst.sql", "some/location/on/classpath/someScriptSecond.sql"])
public class SomeTest {
//...
}
ResourceDatabasePopulator from jdbc.datasource.init or r2dbc.connection.init when using JDBC or R2DBC consecutively
class DbInitializer {
private static boolean initialized = false;
#Autowired
void initializeDb(ConnectionFactory connectionFactory) {
if (!initialized) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource[] scripts = new Resource[] {
resourceLoader.getResource("classpath:some/location/on/classpath/someScriptFirst.sql"),
resourceLoader.getResource("classpath:some/location/on/classpath/someScriptSecond.sql"),
resourceLoader.getResource("classpath:some/location/on/classpath/someScriptThird.sql")
};
new ResourceDatabasePopulator(scripts).populate(connectionFactory).block();
initialized = true;
}
}
}
#SpringBootTest
#Import(DbInitializer.class)
public class SomeTest {
//...
}
Init script in database URI when using JDBC
It is mentioned in offical Testcontainers documentation:
https://www.testcontainers.org/modules/databases/jdbc/
Classpath file:
jdbc:tc:postgresql:9.6.8:///databasename?TC_INITSCRIPT=somepath/init_mysql.sql
File that is not on classpath, but its path is relative to the working directory, which will usually be the project root:
jdbc:tc:postgresql:9.6.8:///databasename?TC_INITSCRIPT=file:src/main/resources/init_mysql.sql
Using an init function:
jdbc:tc:postgresql:9.6.8:///databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction
package org.testcontainers.jdbc;
public class JDBCDriverTest {
public static void sampleInitFunction(Connection connection) throws SQLException {
// e.g. run schema setup or Flyway/liquibase/etc DB migrations here...
}
...
}
When using Spring Boot, I find it easiest to use the JDBC URL support of TestContainers.
You can create a application-integration-test.properties file (typically in src/test/resources with something like this:
spring.datasource.url=jdbc:tc:postgresql://localhost/myappdb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
# This line is only needed if you are using flyway for database migrations
# and not using the default location of `db/migration`
spring.flyway.locations=classpath:db/migration/postgresql
Note the :tc part in the JDBC url.
You can now write a unit test like this:
#RunWith(SpringRunner.class)
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) #ActiveProfiles("integration-test")
public class UserRepositoryIntegrationTest {
#Autowired
private MyObjectRepository repository;
#PersistenceContext
private EntityManager entityManager;
#Autowired
private JdbcTemplate template;
#Test
public void test() {
// use your Spring Data repository, or the EntityManager or the JdbcTemplate to run your SQL and populate your database.
}
Note: This is explained in Practical Guide to Building an API Back End with Spring Boot, chapter 7 in more detail (Disclaimer: I am the author of the book)
Spring framework provides the ability to execute SQL scripts for test suites or for a test unit. For example:
#Test
#Sql({"/test-schema.sql", "/test-user-data.sql"})
public void userTest {
// execute code that relies on the test schema and test data
}
Here's the documentation.
You can also take a look at Spring Test DBUnit which provides annotations to populate your database for a test unit. It uses XML dataset files.
#Test
#DatabaseSetup(value = "insert.xml")
#DatabaseTearDown(value = "insert.xml")
public void testInsert() throws Exception {
// Inserts "insert.xml" before test execution
// Remove "insert.xml" after test execution
}
Also, you can take a look at DbSetup, which provides a java fluent DSL to populate your database.
There is one more option, if you are defining Postgres container manually without fancy testcontainers JDBC url stuff, not related to Spring directly. Postgres image allows to link directory containing sql scripts to container volume and auto-executes them.
GenericContainer pgDb = new PostgreSQLContainer("postgres:9.4-alpine")
.withFileSystemBind("migrations/sqls", "/docker-entrypoint-initdb.d",
BindMode.READ_ONLY)
Also if you need something in runtime, you can always do
pgDb.execInContainer("psql ....").
You can use DatabaseRider, which uses DBUnit behind the scenes, for populating test database and TestContainers as the test datasource. Following is a sample test, full source code is available on github here.
#RunWith(SpringRunner.class)
#SpringBootTest
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) #ActiveProfiles("integration-test")
#DBRider //enables database rider in spring tests
#DBUnit(caseInsensitiveStrategy = Orthography.LOWERCASE) //https://stackoverflow.com/questions/43111996/why-postgresql-does-not-like-uppercase-table-names
public class SpringBootDBUnitIt {
private static final PostgreSQLContainer postgres = new PostgreSQLContainer(); //creates the database for all tests on this file
#PersistenceContext
private EntityManager entityManager;
#Autowired
private UserRepository userRepository;
#BeforeClass
public static void setupContainer() {
postgres.start();
}
#AfterClass
public static void shutdown() {
postgres.stop();
}
#Test
#DataSet("users.yml")
public void shouldListUsers() throws Exception {
assertThat(userRepository).isNotNull();
assertThat(userRepository.count()).isEqualTo(3);
assertThat(userRepository.findByEmail("springboot#gmail.com")).isEqualTo(new User(3));
}
#Test
#DataSet("users.yml") //users table will be cleaned before the test because default seeding strategy
#ExpectedDataSet("expected_users.yml")
public void shouldDeleteUser() throws Exception {
assertThat(userRepository).isNotNull();
assertThat(userRepository.count()).isEqualTo(3);
userRepository.delete(userRepository.findOne(2L));
entityManager.flush();//can't SpringBoot autoconfigure flushmode as commit/always
//assertThat(userRepository.count()).isEqualTo(2); //assertion is made by #ExpectedDataset
}
#Test
#DataSet(cleanBefore = true)//as we didn't declared a dataset DBUnit wont clear the table
#ExpectedDataSet("user.yml")
public void shouldInsertUser() throws Exception {
assertThat(userRepository).isNotNull();
assertThat(userRepository.count()).isEqualTo(0);
userRepository.save(new User("newUser#gmail.com", "new user"));
entityManager.flush();//can't SpringBoot autoconfigure flushmode as commit/always
//assertThat(userRepository.count()).isEqualTo(1); //assertion is made by #ExpectedDataset
}
}
src/test/resources/application-integration-test.properties
spring.datasource.url=jdbc:tc:postgresql://localhost/test
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
#spring.jpa.properties.org.hibernate.flushMode=ALWAYS #doesn't take effect
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
And finally the datasets:
src/test/resources/datasets/users.yml
users:
- ID: 1
EMAIL: "dbunit#gmail.com"
NAME: "dbunit"
- ID: 2
EMAIL: "rmpestano#gmail.com"
NAME: "rmpestano"
- ID: 3
EMAIL: "springboot#gmail.com"
NAME: "springboot"
src/test/resources/datasets/expected_users.yml
users:
- ID: 1
EMAIL: "dbunit#gmail.com"
NAME: "dbunit"
- ID: 3
EMAIL: "springboot#gmail.com"
NAME: "springboot"
src/test/resources/datasets/user.yml
users:
- ID: "regex:\\d+"
EMAIL: "newUser#gmail.com"
NAME: "new user"
After some reviews, I think that it is interesting to review the examples from Spring Data JDBC which use Test Containers:
Note: Use Java 8
git clone https://github.com/spring-projects/spring-data-jdbc.git
mvn clean install -Pall-dbs
I will create a simple project adding some ideas about previous project referenced.
Juan Antonio
Related
In order to setup test data for my Spring Boot integration tests, I'd like to create some helper classes and methods which populate the data using the repositories.
Here is an example:
#Component
public class TestUtils {
private static TemplateRepository templateRepository;
#Autowired
public TestUtils(TemplateRepository templateRepository) {
TestUtils.templateRepository = templateRepository;
}
public static void createTemplates() {
Template template = Template.builder()
.content("some content")
.build();
templateRepository.save(template);
}
}
Due to a lack of experience, I cannot tell if this approach is fine. It it "safe" to inject the repository as static? Or are there better approaches for setting up test data?
Don't use static. If you want to use Java to initialize the data in the repository, just do so in your test.
What you can do if you need to create a few things in different repositories is create a dedicated component:
#Component
public class DatabaseInitializer {
private final TemplateRepository templateRepository;
private final MyOtherRepository myOtherRepository;
// Add constructor here
public void createInitialData() {
// Use repositories to persist some data
}
#ExtendWith(SpringExtension.class)
#Import(DatabaseInitializer.class)
class MyTest {
#Autowired
private DatabaseInitializer initDb;
#Test
void myTest() {
initDb.createInitialData(); // Or put this in a `#Before..` method
// actual test here
}
}
I use TestContainers and Flyway.
You can make SQL scripts and annotate test methods with #Sql and provide a .sql file and/or statements to be run.
You can store these .sql files in the test/resources folder.
Loading Initial Test Data
There is a very well explained process to initialize the data in docs. I would advice you to refer below
https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-initialization
You just have to manintain Insert statements in predefined sql files.
I created simple Camunda spring boot project and also created simple BPMN process with switcher. (5.5 KB)
I used service task with external implementation as a spring beans. I want to write tests for process but I don't want to test how beans works. Because in general I use external implementation for connection to DB and save parameter to context or REST call to internal apps. For example I want skip execute service task(one) and instead set variables for switcher. I tried to use camunda-bpm-assert-scenario for test process and wrote simple test WorkflowTest.
I noticed if I use #MockBean for One.class then Camunda skip delegate execution. If use #Mock then Camunda execute delegate execution.
PS Sorry for bad english
One
#Service
public class One implements JavaDelegate {
private final Random random = new Random();
#Override
public void execute(DelegateExecution execution) throws Exception {
System.out.println("Hello, One!");
execution.setVariable("check", isValue());
}
public boolean isValue() {
return random.nextBoolean();
}
}
WorkflowTest
#SpringBootTest
#RunWith(SpringRunner.class)
#Deployment(resources = "process.bpmn")
public class WorkflowTest extends AbstractProcessEngineRuleTest {
#Mock
private ProcessScenario insuranceApplication;
#MockBean
private One one;
#Before
public void init() {
MockitoAnnotations.initMocks(this);
Mocks.register("one", one);
}
#Test
public void shouldExecuteHappyPath() throws Exception {
// given
when(insuranceApplication.waitsAtServiceTask("Task_generator")).thenReturn(externalTaskDelegate -> {
externalTaskDelegate.complete(withVariables("check", true));
}
);
String processDefinitionKey = "camunda-test-process";
Scenario scenario = Scenario.run(insuranceApplication)
.startByKey(processDefinitionKey) // either just start process by key ...
.execute();
verify(insuranceApplication).hasFinished("end_true");
verify(insuranceApplication, never()).hasStarted("three");
verify(insuranceApplication, atLeastOnce()).hasStarted("two");
assertThat(scenario.instance(insuranceApplication)).variables().containsEntry("check", true);
}
}
I found two solutions:
It's a little hack. If user #MockBean for delegate in a test. The delegate will be skipped but you have trouble with process engine variables.
Create two beans with one qualifier and use profiles for testing and production. I used to default profile for local start and test profile for testing.
I'm doing an integration test in spring and in this example I testing a service layer.
I have a problem where during the addition test in the service, rollback not working, and always add an item to the base ,but not delete it.
I put annotation # Transactional and # TestPropertySource on test class,
also have application-test.properties and the test is successful but the rollback is not executed and a new item is always added to the test database.
My test class and test method for add address(last one):
#RunWith(SpringRunner.class)
#SpringBootTest(classes = TicketServiceApplication.class)
#Transactional
#TestPropertySource("classpath:application-test.properties")
public class AddressServiceIntegrationTest {
#Autowired
private AddressService addressService;
#Autowired
private AddressRepository addressRepository;
#Test
public void findAllSuccessTest(){
List<Address> result = addressService.finfAllAddress();
assertNotNull(result);
assertFalse(result.isEmpty());
assertEquals(2, result.size());
}
#Test
public void findOneAddressExistTest_thenReturnAddress(){
Long id = AddressConst.VALID_ID_ADDRESS;
Address a = addressService.findOneAddress(id);
assertEquals(id, a.getId());
}
#Test(expected = EntityNotFoundException.class)
public void findOneAddressNotExistTest_thenThrowException(){
Long id = AddressConst.NOT_VALID_ID_ADDRESS;
Address a = addressService.findOneAddress(id);
}
#Test
public void addAddressSuccessTest(){
int sizeBeforeAdd = addressRepository.findAll().size();
Address address = AddressConst.newAddressToAdd();
Address result = addressService.addAddress(address);
int sizeAfterAdd = addressRepository.findAll().size();
assertNotNull(result);
assertEquals(sizeBeforeAdd+1, sizeAfterAdd);
assertEquals(address.getCity(), result.getCity());
assertEquals(address.getState(), result.getState());
assertEquals(address.getNumber(), result.getNumber());
assertEquals(address.getLatitude(), result.getLatitude());
assertEquals(address.getLongitude(), result.getLongitude());
assertEquals(address.getStreet(), result.getStreet());
}
}
My application-test.properties :
spring.datasource.url= jdbc:mysql://localhost:3306/kts_test&useSSL=false&
useUnicode=true&characterEncoding=utf8
spring.datasource.username = root
spring.datasource.password = root
spring.jpa.show-sql = true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
And when execute add address test, every time in my kts_test data base ( db used for testing) is added new item and not rollback.
This is a log from the console where you can see that the rollback was called but did not execute, because when I refresh the database after the test the new item was left, it was not deleted.
INFO 10216 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext#3e58a80e testClass = AddressServiceIntegrationTest, testInstance = com.ftn.services.address.AddressServiceIntegrationTest#4678ec43, testMethod = addAddressSuccessTest#AddressServiceIntegrationTest, testException = [null],...
Lastly, to write that I tried #Transactional above methods, I also tried # Rollback above methods or # Rollback (true), tried changing application-test.properties and I no longer know what the error might be.
If anyone can help I would be grateful. Thank you.
Hibernate is by default creating tables with MyISAM storage engine - this engine does not support transactions. You need to have InnoDB tables:
Either manage your DB migrations manually with a tool like Flyway (imho preferred option so you can have a complete control) and turn off recreating database in tests.
Or set the correct engine in configuration:
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
OR (deprecated)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
EDIT: As C. Weber suggested in the comments, the solution is to add #Transactional to the test class.
I have some tests that use an H2 in-memory DB. I need to reset the DB before each test. Although my SQL scripts are run each a test is executed, the DB is not properly reset, resulting in a missing needed entry after a delete test.
Test class:
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureTestDatabase(replace=Replace.ANY, connection=EmbeddedDatabaseConnection.H2)
public class RepositoryTests {
#Autowired
private Repository repository;
#Autowired
private DataSource dataSource;
#Before
public void populateDb() {
Resource initSchema = new ClassPathResource("database/schema.sql");
Resource initData = new ClassPathResource("database/data.sql");
DatabasePopulator dbPopulator = new ResourceDatabasePopulator(initSchema, initData);
DatabasePopulatorUtils.execute(dbPopulator, dataSource);
}
#Test
public void testMethod1() {
// ...
repository.delete("testdata");
}
#Test
public void testMethod2() {
// ...
Object test = repository.get("testdata");
// is null but should be an instance
}
}
schema.sql drops all tables before recreating them. data.sql inserts all needed test data into the DB.
Running the testMethod2 alone succeeds. However, running all tests makes the test fail with a NullPointerException.
I have successfully tried to use #DirtiesContext, however this is not an option because I can't afford to have a 20 second startup for each 0.1 second test.
Is there another solution?
The Spring Test Framework provides a mechanism for the behaviour you want for your tests. Simply annotate your Test class with #Transactional to get the default rollback behaviour for each test method.
There are ways to configure the transactional behaviour of tests and also some pitfalls (like using RestTemplate inside test method), which you can read more about in the corresponding chapter of the Spring manual.
Spring Test Framework
I use the spring-boot-starter-web and spring-boot-starter-test.
Let's say I have a class for binding configuration properties:
#ConfigurationProperties(prefix = "dummy")
public class DummyProperties {
#URL
private String url;
// getter, setter ...
}
Now I want to test that my bean validation is correct. The context should fail to start (with a specfic error message) if the property dummy.value is not set or if it contains an invalid URL. The context should start if the property contains a valid URL. (The test would show that #NotNull is missing.)
A test class would look like this:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MyApplication.class)
#IntegrationTest({ "dummy.url=123:456" })
public class InvalidUrlTest {
// my test code
}
This test would fail because the provided property is invalid. What would be the best way to tell Spring/JUnit: "yep, this error is expected". In plain JUnit tests I would use the ExpectedException.
The best way to test Spring application context is to use ApplicationContextRunner
It is described in Spring Boot Reference Documentation:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-test-autoconfig
And there is a quick guide about it:
https://www.baeldung.com/spring-boot-context-runner
Sample usage
private static final String POSITIVE_CASE_CONFIG_FILE =
"classpath:some/path/positive-case-config.yml";
private static final String NEGATIVE_CASE_CONFIG_FILE =
"classpath:some/path/negative-case-config.yml";
#Test
void positiveTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + POSITIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
Assertions.assertThat(context).hasNotFailed();//6
});
}
#Test
void negativeTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + NEGATIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
assertThat(context)
.hasFailed();
assertThat(context.getStartupFailure())
.isNotNull();
assertThat(context.getStartupFailure().getMessage())
.contains("Some exception message");
assertThat(extractFailureCauseMessages(context))
.contains("Cause exception message");
});
}
private List<String> extractFailureCauseMessages(AssertableApplicationContext context) {
var failureCauseMessages = new ArrayList<String>();
var currentCause = context.getStartupFailure().getCause();
while (!Objects.isNull(currentCause)) {//7
failureCauseMessages.add(currentCause.getMessage());
currentCause = currentCause.getCause();
}
return failureCauseMessages;
}
Explanation with examples of similar definitions from Junit5 Spring Boot Test Annotations:
Triggers loading of config files like application.properties or application.yml
Logs ConditionEvaluationReport using given log level when application context fails
Provides class that specifies mock beans, ie. we have #Autowired BookRepository in our BookService and we provide mock BookRepository in MockBeansTestConfiguration. Similar to #Import({MockBeansTestConfiguration.class}) in test class and #TestConfiguration in class with mock beans in normal Junit5 Spring Boot Test
Equivalent of #TestPropertySource(properties = { "spring.config.location=" + POSITIVE_CASE_CONFIG_FILE})
Triggers spring auto configuration for given class, not direct equivalent, but it is similar to using #ContextConfiguration(classes = {BookService.class}) or #SpringBootTest(classes = {BookService.class}) together with #Import({BookService.class}) in normal test
Assertions.class from AssertJ library, there should be static import for Assertions.assertThat, but I wanted to show where this method is from
There should be static import for Objects.isNull, but I wanted to show where this method is from
MockBeansTestConfiguration class:
#TestConfiguration
public class MockBeansTestConfiguration {
private static final Book SAMPLE_BOOK = Book.of(1L, "Stanisław Lem", "Solaris", "978-3-16-148410-0");
#Bean
public BookRepository mockBookRepository() {
var bookRepository = Mockito.mock(BookRepository.class);//1
Mockito.when(bookRepository.findByIsbn(SAMPLE_BOOK.getIsbn()))//2
.thenReturn(SAMPLE_BOOK);
return bookRepository;
}
}
Remarks:
1,2. There should be static import, but I wanted to show where this method is from
Why is that an integration test to begin with? Why are you starting a full blown Spring Boot app for that?
This looks like unit testing to me. That being said, you have several options:
Don't add #IntegrationTest and Spring Boot will not start a web server to begin with (use #PropertySource to pass value to your test but it feels wrong to pass an invalid value to your whole test class)
You can use spring.main.web-environment=false to disable the web server (but that's silly given the point above)
Write a unit test that process that DummyProperties of yours. You don't even need to start a Spring Boot application for that. Look at our own test suite
I'd definitely go with the last one. Maybe you have a good reason to have an integration test for that?
I think the easiest way is:
public class InvalidUrlTest {
#Rule
public DisableOnDebug testTimeout = new DisableOnDebug(new Timeout(5, TimeUnit.SECONDS));
#Rule
public ExpectedException expected = ExpectedException.none();
#Test
public void shouldFailOnStartIfUrlInvalid() {
// configure ExpectedException
expected.expect(...
MyApplication.main("--dummy.url=123:456");
}
// other cases
}