How to write Transactional Unit Tests with Spring
Spring is a great framework to develop enterprise Java web applications. It provides tons of features for us. One of them is its TestContext Framework, which helps us to implement integration unit tests easily in our enterprise applications.
Integration unit tests may cover several layers and include ApplicationContext loading, transactional persistence operations, security checks and so on. In this example, we will show you how to write transactional integration unit tests in your enterprise application so that you can be sure that your data access logic or persistence operations work as expected within an active transaction context.
Table Of Contents
- 1. Create a new Maven Project
- 2. Add necessary dependencies in your project
- 3. Create log4j.xml file in your project
- 4. Prepare DDL and DML scripts to initialize database
- 5. Write Domain Class, Service and DAO Beans
- 6. Configure Spring ApplicationContext
- 7. Write a transactional integration unit test
- 8. Run the tests and observe the results
- 9. Summary
- 10. Download the Source Code
Our preferred development environment is Spring Tool Suite 3.8.2 based on Eclipse 4.6.1 version. However, as we are going to create the example as maven project, you can easily work within your own IDE as well. We are also using Spring Application Framework 4.3.1.RELEASE along with JDK 1.8_u112, and H2 database version 1.4.192.
1. Create a new Maven Project
Write click on Package Explorer and select New>Maven Project to create an new maven project by skipping archetype selection. This will create a simple maven project.
Click pom.xml in the project root folder in order to open up pom.xml editor, and add maven.compiler.source and maven.compiler.target properties with value 1.8 into it.
2. Add necessary dependencies in your project
Add following dependencies into your pom.xml. You can make use of pom.xml editor you opened up in the previous step.
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.192</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.1.RELEASE</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
You can either add those dependencies via add Dependency dialog, or switch into source view of pom.xml and copy all of them into section. After this step, added dependencies should have been listed as follows.
Finally perform a project update by right clicking the project and then clicking “Update Project” through Maven>Update Project…
At this point, you are ready to work within the project.
3. Create log4j.xml file in your project
The first step is to create log4j.xml file under src/main/resources folder with the following content. It will help us to see log messages produced by Spring during execution of test methods and trace what is going on during those executions.
log4j.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration PUBLIC "-//LOG4J" "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.EnhancedPatternLayout"> <param name="ConversionPattern" value="%d{HH:mm:ss,SSS} - %p - %C{1.}.%M(%L): %m%n" /> </layout> </appender> <logger name="org.springframework"> <level value="DEBUG" /> </logger> <root> <level value="INFO" /> <appender-ref ref="CONSOLE" /> </root> </log4j:configuration>
4. Prepare DDL and DML scripts to initialize database
Create schema.sql and data.sql files within src/main/resources with the following contents.
schema.sql
CREATE SEQUENCE PUBLIC.T_PERSON_SEQUENCE START WITH 1; CREATE CACHED TABLE PUBLIC.T_PERSON( ID BIGINT NOT NULL, FIRST_NAME VARCHAR(255), LAST_NAME VARCHAR(255) ); ALTER TABLE PUBLIC.T_PERSON ADD CONSTRAINT PUBLIC.CONSTRAINT_PERSON_PK PRIMARY KEY(ID);
data.sql
INSERT INTO T_PERSON (ID,FIRST_NAME,LAST_NAME) VALUES (T_PERSON_SEQUENCE.NEXTVAL, 'John','Doe'); INSERT INTO T_PERSON (ID,FIRST_NAME,LAST_NAME) VALUES (T_PERSON_SEQUENCE.NEXTVAL, 'Joe','Doe');
5. Write Domain Class, Service and DAO Beans
We are going to create a simple domain class with name Person as follows. It has only three attributes, id, firstName and lastName, and accessor methods for them.
Person.java
package com.example.model; public class Person { private Long id; private String firstName; private String lastName; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
We also create Service and DAO classes as follows, in order to perform simple persistence operations with our domain model.
PersonDao.java
package com.example.dao; import com.example.model.Person; public interface PersonDao { public Person findById(Long id); public void create(Person person); public void update(Person person); public void delete(Long id); }
PersonDao is a simple interface which defines basic persistence operations over Person instances like findById, create a new Person, update or delete an existing one.
JdbcPersonDao.java
package com.example.dao; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import com.example.model.Person; @Repository public class JdbcPersonDao implements PersonDao { private JdbcTemplate jdbcTemplate; @Autowired public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public Person findById(Long id) { return jdbcTemplate.queryForObject("select first_name, last_name from t_person where id = ?", new RowMapper() { @Override public Person mapRow(ResultSet rs, int rowNum) throws SQLException { Person person = new Person(); person.setId(id); person.setFirstName(rs.getString("first_name")); person.setLastName(rs.getString("last_name")); return person; } }, id); } @Override public void create(Person person) { jdbcTemplate.update("insert into t_person(id,first_name,last_name) values(t_person_sequence.nextval,?,?)", person.getFirstName(), person.getLastName()); } @Override public void update(Person person) { jdbcTemplate.update("update t_person set first_name = ?, last_name = ? where id = ?", person.getFirstName(), person.getLastName(), person.getId()); } @Override public void delete(Long id) { jdbcTemplate.update("delete from t_person where id = ?", id); } }
JdbcPersonDao is an implementation of PersonDao interface which employs NamedParameterJdbcTemplate bean of Spring in order to implement persistence operations via JDBC API.
PersonService.java
package com.example.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.dao.PersonDao; import com.example.model.Person; @Service @Transactional public class PersonService { private PersonDao personDao; @Autowired public void setPersonDao(PersonDao personDao) { this.personDao = personDao; } public Person findById(Long id) { return personDao.findById(id); } public void create(Person person) { personDao.create(person); } public void update(Person person) { personDao.update(person); } public void delete(Long id) { personDao.delete(id); } }
PersonService is a transactional service which uses PersonDao bean in order to perform persistence operations. Its role is simply delegating to its DAO bean apart from being transactional in this context.
6. Configure Spring ApplicationContext
Write click onver src/main/resources and create a new Spring Bean Definition File through “New>Spring Bean Configuration File”. Make sure you select context, tx and jdbc namespaces as you create the configuration file.
spring-beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd"> <context:component-scan base-package="com.example"/> <tx:annotation-driven/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <jdbc:embedded-database type="H2" id="dataSource"> <jdbc:script location="classpath:/schema.sql"/> <jdbc:script location="classpath:/data.sql"/> </jdbc:embedded-database> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> </beans>
7. Write a transactional integration unit test
PersonServiceIntegrationTests.java
package com.example; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import com.example.model.Person; import com.example.service.PersonService; @RunWith(SpringJUnit4ClassRunner.class) @Transactional @ContextConfiguration("classpath:/spring-beans.xml") public class PersonServiceIntegrationTests { @Autowired private PersonService personService; @Autowired private JdbcTemplate jdbcTemplate; @Test public void shouldCreateNewPerson() { Person person = new Person(); person.setFirstName("Kenan"); person.setLastName("Sevindik"); long countBeforeInsert = jdbcTemplate.queryForObject("select count(*) from t_person", Long.class); Assert.assertEquals(2, countBeforeInsert); personService.create(person); long countAfterInsert = jdbcTemplate.queryForObject("select count(*) from t_person", Long.class); Assert.assertEquals(3, countAfterInsert); } @Test public void shouldDeleteNewPerson() { long countBeforeDelete = jdbcTemplate.queryForObject("select count(*) from t_person", Long.class); Assert.assertEquals(2, countBeforeDelete); personService.delete(1L); long countAfterDelete = jdbcTemplate.queryForObject("select count(*) from t_person", Long.class); Assert.assertEquals(1, countAfterDelete); } @Test public void shouldFindPersonsById() { Person person = personService.findById(1L); Assert.assertNotNull(person); Assert.assertEquals("John", person.getFirstName()); Assert.assertEquals("Doe", person.getLastName()); } }
Above test methods test creation of a new Person instance, deletion of an existing one and finding by its id.
@RunWith annotation belongs to Junit, and is used to tell IDE which Runner class, SpringJUnit4ClassRunner.class in this case, to use to run test methods defined in the class. SpringJUnit4ClassRunner creates an ApplicationContext by loading Spring bean configuration files listed in @ContextConfiguration(“classpath:/spring-beans.xml”) annotation. It is possible to use Java Configuration classes as well, however, I preferred to follow classical XML way in this example. After creation of ApplicationContext, dependencies specified in the test class are autowired for use within test methods. @Transactional annotation tells SpringJUnit4ClassRunner that all test methods defined in this class must be run within an active transaction context.
Therefore, SpringJUnit4ClassRunner starts a new transaction at the beginning of each test method execution, and then rolls back it at the end. The reason to rollback instead of commit is that those changes performed on the database within each test method should not adversely affect execution of other integration tests. However, any service method call which expects an active transaction to work during its execution is satisfied with that active transaction spanning the test method. It is possible to see how transaction is created and then rolled back from the log messages shown below.
8. Run the tests and observe the results
Right click over the test class, and run it with JUnit. You should have seen all JUnit tests passed as follows.
When you click over the console tab, you should have seen log messages similar to the following.
17:51:24,230 - DEBUG - o.s.t.c.t.TransactionalTestExecutionListener.beforeTestMethod(183): Explicit transaction definition [PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''] found for test context [DefaultTestContext@3e6fa38a testClass = PersonServiceIntegrationTests, testInstance = com.example.PersonServiceIntegrationTests@6a4f787b, testMethod = shouldCreateNewPerson@PersonServiceIntegrationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@66a3ffec testClass = PersonServiceIntegrationTests, locations = '{classpath:/spring-beans.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]] 17:51:24,230 - DEBUG - o.s.t.c.t.TransactionalTestExecutionListener.retrieveConfigurationAttributes(476): Retrieved @TransactionConfiguration [null] for test class [com.example.PersonServiceIntegrationTests]. 17:51:24,230 - DEBUG - o.s.t.c.t.TransactionalTestExecutionListener.retrieveConfigurationAttributes(483): Using TransactionConfigurationAttributes [TransactionConfigurationAttributes@5167f57d transactionManagerName = '', defaultRollback = true] for test class [com.example.PersonServiceIntegrationTests]. 17:51:24,230 - DEBUG - o.s.t.c.c.DefaultCacheAwareContextLoaderDelegate.loadContext(129): Retrieved ApplicationContext from cache with key [[MergedContextConfiguration@66a3ffec testClass = PersonServiceIntegrationTests, locations = '{classpath:/spring-beans.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]] 17:51:24,230 - DEBUG - o.s.t.c.c.DefaultContextCache.logStatistics(290): Spring test ApplicationContext cache statistics: [DefaultContextCache@2fb0623e size = 1, maxSize = 32, parentContextCount = 0, hitCount = 1, missCount = 1] 17:51:24,231 - DEBUG - o.s.b.f.s.AbstractBeanFactory.doGetBean(251): Returning cached instance of singleton bean 'transactionManager' 17:51:24,231 - DEBUG - o.s.t.c.t.TransactionalTestExecutionListener.isRollback(426): No method-level @Rollback override: using default rollback [true] for test context [DefaultTestContext@3e6fa38a testClass = PersonServiceIntegrationTests, testInstance = com.example.PersonServiceIntegrationTests@6a4f787b, testMethod = shouldCreateNewPerson@PersonServiceIntegrationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@66a3ffec testClass = PersonServiceIntegrationTests, locations = '{classpath:/spring-beans.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]. 17:51:24,232 - DEBUG - o.s.t.s.AbstractPlatformTransactionManager.getTransaction(367): Creating new transaction with name [com.example.PersonServiceIntegrationTests.shouldCreateNewPerson]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '' 17:51:24,233 - DEBUG - o.s.j.d.SimpleDriverDataSource.getConnectionFromDriver(138): Creating new JDBC Driver Connection to [jdbc:h2:mem:dataSource;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false] 17:51:24,233 - DEBUG - o.s.j.d.DataSourceTransactionManager.doBegin(206): Acquired Connection [conn1: url=jdbc:h2:mem:dataSource user=SA] for JDBC transaction 17:51:24,234 - DEBUG - o.s.j.d.DataSourceTransactionManager.doBegin(223): Switching JDBC Connection [conn1: url=jdbc:h2:mem:dataSource user=SA] to manual commit 17:51:24,234 - INFO - o.s.t.c.t.TransactionContext.startTransaction(101): Began transaction (1) for test context [DefaultTestContext@3e6fa38a testClass = PersonServiceIntegrationTests, testInstance = com.example.PersonServiceIntegrationTests@6a4f787b, testMethod = shouldCreateNewPerson@PersonServiceIntegrationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@66a3ffec testClass = PersonServiceIntegrationTests, locations = '{classpath:/spring-beans.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]; transaction manager [org.springframework.jdbc.datasource.DataSourceTransactionManager@2eea88a1]; rollback [true] 17:51:24,236 - DEBUG - o.s.j.c.JdbcTemplate.query(451): Executing SQL query [select count(*) from t_person] 17:51:24,253 - DEBUG - o.s.b.f.s.AbstractBeanFactory.doGetBean(251): Returning cached instance of singleton bean 'transactionManager' 17:51:24,253 - DEBUG - o.s.t.s.AbstractPlatformTransactionManager.handleExistingTransaction(476): Participating in existing transaction 17:51:24,273 - DEBUG - o.s.j.c.JdbcTemplate.update(869): Executing prepared SQL update 17:51:24,274 - DEBUG - o.s.j.c.JdbcTemplate.execute(616): Executing prepared SQL statement [insert into t_person(id,first_name,last_name) values(t_person_sequence.nextval,?,?)] 17:51:24,279 - DEBUG - o.s.j.c.JdbcTemplate$2.doInPreparedStatement(879): SQL update affected 1 rows 17:51:24,279 - DEBUG - o.s.j.c.JdbcTemplate.query(451): Executing SQL query [select count(*) from t_person] 17:51:24,281 - DEBUG - o.s.t.s.AbstractPlatformTransactionManager.processRollback(851): Initiating transaction rollback 17:51:24,281 - DEBUG - o.s.j.d.DataSourceTransactionManager.doRollback(284): Rolling back JDBC transaction on Connection [conn1: url=jdbc:h2:mem:dataSource user=SA] 17:51:24,283 - DEBUG - o.s.j.d.DataSourceTransactionManager.doCleanupAfterCompletion(327): Releasing JDBC Connection [conn1: url=jdbc:h2:mem:dataSource user=SA] after transaction 17:51:24,283 - DEBUG - o.s.j.d.DataSourceUtils.doReleaseConnection(327): Returning JDBC Connection to DataSource 17:51:24,283 - INFO - o.s.t.c.t.TransactionContext.endTransaction(136): Rolled back transaction for test context [DefaultTestContext@3e6fa38a testClass = PersonServiceIntegrationTests, testInstance = com.example.PersonServiceIntegrationTests@6a4f787b, testMethod = shouldCreateNewPerson@PersonServiceIntegrationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@66a3ffec testClass = PersonServiceIntegrationTests, locations = '{classpath:/spring-beans.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]].
Sometimes, you may need test executions to commit, instead of rollback so that you can connect to database, and observe the changes performed there, or you may employ integration unit tests for populating database with sample data. You can place either @Rollback(false) or @Commit annotations either on method or class level so that transaction commits instead of rollback.
9. Summary
In this example, we created a maven project, implemented several classes to perform persistence operations using JDBC API within it, and wrote an integration unit test in order to check whether those classes perform necessary persistence related operations as expected within an active transaction.
10. Download the Source Code
You can download the full source code of this example here: HowToWriteTransactionalTestsInSpring
Transactional Unit Test is an Oxymoron!