spring

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.
 
 
 

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.

Let’s begin.

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.

create new maven project
Create New Maven Project

configure maven project
Configure 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.

click pom.xml to edit
Click pom.xml to edit

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>

add maven dependencies
Add Maven Dependencies

maven add a new dependency
Add a New 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.

maven added dependencies
Maven Added Dependencies

Finally perform a project update by right clicking the project and then clicking “Update Project” through Maven>Update Project…

update maven project
Update Maven 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.

junit test results
JUnit Test Results

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

Download
You can download the full source code of this example here: HowToWriteTransactionalTestsInSpring

Kenan Sevindik

Develops enterprise software, gives training, mentoring and consultancy services about Java, Design Patterns, OOP, AOP, Spring, Spring Security, Vaadin and Hibernate. Specialized on the architecture and development of enterprise applications using various Java technologies for more than 15 years. Works with various enterprise Java frameworks, such as Spring Application Framework, Spring Security Framework, Hibernate Persistence Framework since their initial phases. Co-author of "Beginning Spring" book published by Wiley Publishing in February 2015. Public speaker. B.S. in Computer Engineering.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Woody Woodpecker
Woody Woodpecker
3 years ago

Transactional Unit Test is an Oxymoron!

Back to top button