Lightweight Integration Testing
In this code-heavy tutorial, learn how to perform light-weight integration testing by implementing a login management system.
Join the DZone community and get the full member experience.
Join For FreeVirtually all enterprise applications use data and require communication with databases and hence some kind of persistence layer implementation. Provided having permanently increasing requirements for application availability and flexibility, it becomes critical to have the possibility to test the persistence layer quickly and effectively.
Suppose we are to implement a login management system. According to the specifications, it should be implemented as a microservice and provide a possibility to retrieve login information from the underlying database.
The domain entity and data access object are presented in Listing 1 and Listing 2, respectively.
Listing 1: LoginInfo entity
@NamedNativeQueries({
@NamedNativeQuery(name = LoginInfo.FIND_LATEST_LOGINS,
query = "SELECT DISTINCT li1.deviceInfo FROM (SELECT li.deviceInfo FROM LoginInfo li WHERE li.userId = :userId ORDER BY li.loginDate DESC) li1")
})
@JsonbPropertyOrder({"deviceData", "loginTimestamp"})
@Entity
public class LoginInfo implements Serializable {
private static final long serialVersionUID = 1L;
public static final String FIND_LATEST_LOGINS = "findLatestLogins";
@Id
@SequenceGenerator(name = "LoginInfo_Generator", sequenceName = "LoginInfo_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "LoginInfo_Generator")
private long id;
private String userId;
@JsonbProperty("deviceData")
private String deviceInfo;
@JsonbProperty("loginTimestamp")
private LocalDateTime loginDate;
// getters and setters . . .
Listing 2: LoginInfoDao class
public class LoginInfoDao {
@PersistenceContext(name = "login-info-model", unitName = "loginInfoPU")
private EntityManager em;
public List<String> findLatesLogins(String userId) {
return em.createNamedQuery(LoginInfo.FIND_LATEST_LOGINS)
.setParameter("userId", userId)
.setMaxResults(5)
.getResultList();
}
public void create(LoginInfo loginInfoData) {
em.persist(loginInfoData);
}
}
Our task is to develop a quick and comprehensive test for checking the DAO functionality. One option is to use Arquillian Persistence extension, which enriches Arquillian dynamic deployment with DBUnit support and provides a possibility to use annotations for handling test data.
Usage of Arquillian Persistence Extension for Integration Testing
First of all, we need to add necessary dependencies to our PM file (assume we use the wildfly-swarm container):
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-persistence-dbunit</artifactId>
<version>1.0.0.Alpha7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wildfly.swarm</groupId>
<artifactId>arquillian</artifactId>
<scope>test</scope>
</dependency>
Then, we add test persistence.xml descriptor, Arquillian descriptor, and DBUnit XML file containing test data:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.1" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="loginInfoPU" transaction-type="JTA">
<description>LoginInfo Persistence Unit</description>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="javax.persistence.schema-generation.scripts.action" value="drop-and-create"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
<arquillian xmlns="http://jboss.org/schema/arquillian"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.org/schema/arquillian
http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
<!-- Configuration to be used when the WildFly managed profile is active -->
<container qualifier="widlfly-managed" default="true">
<configuration>
<property name="javaVmArguments">-DIGDB_API_KEY=dummyKey -DIGDB_HOST=http://127.0.0.1:8071</property>
</configuration>
</container>
<extension qualifier="persistence">
<property name="defaultDataSource">java:jboss/datasources/ExampleDS</property>
<property name="scriptsToExecuteBeforeTest">SET REFERENTIAL_INTEGRITY FALSE;</property>
<property name="userTransactionJndi">java:jboss/UserTransaction</property>
<property name="dumpData">true</property>
<property name="dumpDirectory">/tmp/showcase</property>
</extension>
</arquillian>
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<LoginInfo id="1" userId="777" deviceInfo="X-User-Agent: Mozilla" loginDate="2017-12-08" />
<LoginInfo id="2" userId="777" deviceInfo="X-User-Agent: Mozilla" loginDate="2017-12-09" />
<LoginInfo id="3" userId="777" deviceInfo="X-User-Agent: IE" loginDate="2017-12-10" />
<LoginInfo id="4" userId="777" deviceInfo="X-User-Agent: IE" loginDate="2017-12-11" />
<LoginInfo id="7" userId="777" deviceInfo="X-User-Agent: Chrome" loginDate="2017-12-12" />
<LoginInfo id="8" userId="777" deviceInfo="X-User-Agent: Chrome" loginDate="2017-12-12" />
<LoginInfo id="9" userId="777" deviceInfo="X-User-Agent: Opera" loginDate="2017-12-13" />
<LoginInfo id="10" userId="777" deviceInfo="X-User-Agent: Opera" loginDate="2017-12-14" />
<LoginInfo id="11" userId="777" deviceInfo="X-User-Agent: XXX" loginDate="2017-12-19" />
<LoginInfo id="12" userId="777" deviceInfo="X-User-Agent: XX" loginDate="2017-12-19" />
<LoginInfo id="13" userId="777" deviceInfo="X-User-Agent: XX" loginDate="2017-12-29" />
<LoginInfo id="14" userId="777" deviceInfo="X-User-Agent: XXXX" loginDate="2018-12-09" />
</dataset>
Now, we can create the following integration test.
Listing 3: Integration test based on Arquillian Persistence extension
import java.time.LocalDateTime;
import java.util.List;
import javax.annotation.Resource;
import javax.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.persistence.ShouldMatchDataSet;
import org.jboss.arquillian.persistence.UsingDataSet;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.myfirm.loginservice.control.LoginInfoDao;
@RunWith(Arquillian.class)
public class LoginInfoArquillianPersistenceIT {
private static final String TEST_USER_ID = "888";
private static final String TEST_DEVICE_INFO = "X-User-Agent: Chrome";
private static final LocalDateTime TEST_LOGIN_DATE = LocalDateTime.of(2017, 12, 12, 0, 0);
@Deployment
public static WebArchive createDeploymentPackage() {
return ShrinkWrap.create(WebArchive.class, LoginInfoArquillianPersistenceIT.class.getName() + ".war")
.addClasses(
LoginInfoDao.class,
LoginInfo.class
)
.addAsResource("test-persistence.xml", "META-INF/persistence.xml")
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
}
@Inject
private LoginInfoDao loginDao;
@Resource
private javax.transaction.UserTransaction userTransaction;
@Test
@UsingDataSet("datasets/loginInfoTestData.xml")
public void testFindLatestLogins() {
List<String> loginInfoData = loginDao.findLatesLogins("777");
Assert.assertNotNull(loginInfoData);
Assert.assertEquals(5, loginInfoData.size());
}
@Test
@ShouldMatchDataSet(value="datasets/expectedNewLoginInfo.xml", excludeColumns="id")
public void testCreate() {
LoginInfo loginInfo = new LoginInfo();
loginInfo.setUserId(TEST_USER_ID);
loginInfo.setDeviceInfo(TEST_DEVICE_INFO);
loginInfo.setLoginDate(TEST_LOGIN_DATE);
loginDao.create(loginInfo);
}
}
This way, we created a deployment simulation, which allows testing our persistence layer, including JTA data source and CDI.
Sometimes, we can get even more light-weight and isolated. For example, if we need just to test our persistence stuff without any container functionality, we can use application-managed EntityManager and inject it manually. To test JPA stuff in transactional context, we can use the hibernate-testing library, which greatly simplifies transaction handling in integration tests.
Usage of Hibernate-Testing Library for Integration Testing
In this case, we don't want to use any container, so we need to do some additional work for creating a minimalistic JPA environment. So, we create an application-managed EntityManager object and "inject" it into our DAO object with the usage of java.reflection package, see Listing 4.
Listing 4: Persistence integration test with the usage of hibernate-testing library
import static org.hibernate.testing.transaction.TransactionUtil.*;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;
import org.dbunit.Assertion;
import org.dbunit.IDatabaseTester;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.h2.tools.RunScript;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import com.myfirm.loginservice.control.LoginInfoDao;
public class LoginInfoHibernateTestingPersistenceIT {
private static final String JDBC_DRIVER = org.h2.Driver.class.getName();
private static final String JDBC_URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1";
private static final String USER = "sa";
private static final String PASSWORD = "sa";
private static final String TEST_USER_ID = "888";
private static final String TEST_DEVICE_INFO = "X-User-Agent: Chrome";
private static final LocalDateTime TEST_LOGIN_DATE = LocalDateTime.of(2017, 12, 12, 0, 0);
private static final Logger logger = Factory.getLogger(LoginInfoHibernateTestingPersistenceIT.class);
private EntityManagerFactory emf;
private LoginInfoDao loginDao;
private Field emField;
private IDatabaseTester databaseTester;
@BeforeClass
public static void createSchema() throws Exception {
RunScript.execute(JDBC_URL, USER, PASSWORD, "src/test/resources/datasets/createSchema.sql", null, false);
}
public LoginInfoHibernateTestingPersistenceIT() throws NoSuchFieldException, SecurityException, ClassNotFoundException {
loginDao = new LoginInfoDao();
emField = loginDao.getClass().getDeclaredField("em");
emField.setAccessible(true);
databaseTester = new JdbcDatabaseTester(JDBC_DRIVER, JDBC_URL, USER, PASSWORD);
}
@Test
public void testFindLatesLoginsQuery() throws Exception {
cleanlyInsert(getDataSet("src/test/resources/datasets/loginInfoTestData.xml"));
doInJPA(this::entityManagerFactory, entityManager -> {
List<String> loginInfoData = entityManager.createNamedQuery(LoginInfo.FIND_LATEST_LOGINS)
.setParameter("userId", "777")
.setMaxResults(5)
.getResultList();
Assert.assertEquals(5, loginInfoData.size());
Assert.assertTrue(loginInfoData.contains("X-User-Agent: XXXX"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: XXX"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: XX"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: Chrome"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: Opera"));
Assert.assertFalse(loginInfoData.contains("X-User-Agent: IE"));
Assert.assertFalse(loginInfoData.contains("X-User-Agent: Mozilla"));
} );
}
@Test
public void testFindLatesLogins() throws Exception {
cleanlyInsert(getDataSet("src/test/resources/datasets/loginInfoTestData.xml"));
doInJPA(this::entityManagerFactory, entityManager -> {
injectEntityManager(entityManager);
List<String> loginInfoData = loginDao.findLatesLogins("777");
Assert.assertEquals(5, loginInfoData.size());
Assert.assertTrue(loginInfoData.contains("X-User-Agent: XXXX"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: XXX"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: XX"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: Chrome"));
Assert.assertTrue(loginInfoData.contains("X-User-Agent: Opera"));
Assert.assertFalse(loginInfoData.contains("X-User-Agent: IE"));
Assert.assertFalse(loginInfoData.contains("X-User-Agent: Mozilla"));
} );
}
@Test
public void testCreate() throws Exception {
clean(getDataSet("src/test/resources/datasets/loginInfoTestData.xml"));
doInJPA(this::entityManagerFactory, entityManager -> {
injectEntityManager(entityManager);
LoginInfo loginInfo = new LoginInfo();
loginInfo.setUserId(TEST_USER_ID);
loginInfo.setDeviceInfo(TEST_DEVICE_INFO);
loginInfo.setLoginDate(TEST_LOGIN_DATE);
loginDao.create(loginInfo);
} );
//Check the result
try {
ITable actualTable = databaseTester.getConnection()
.createQueryTable("test_query", "SELECT * FROM LoginInfo");
IDataSet expectedDataSet = new FlatXmlDataSetBuilder()
.build(new File("src/test/resources/datasets/expectedNewLoginInfo.xml"));
ITable expectedTable = expectedDataSet.getTable("LoginInfo");
Assertion.assertEquals(expectedTable, actualTable);
} catch (Exception e) {
logger.error("testCreate2() - error while getting test results.", e);
}
}
private EntityManagerFactory entityManagerFactory() {
if (emf == null) {
Map<String, String> props = new HashMap<>();
props.put("javax.persistence.jdbc.driver", JDBC_DRIVER);
props.put("javax.persistence.jdbc.url", JDBC_URL);
props.put("javax.persistence.jdbc.user", USER);
props.put("javax.persistence.jdbc.password", PASSWORD);
emf = Persistence.createEntityManagerFactory("loginInfoPU", props);
}
return emf;
}
private void injectEntityManager(EntityManager em){
try {
emField.set(loginDao, em);
} catch (IllegalArgumentException | IllegalAccessException e) {
logger.error("Error while injecting EntityManager into DeviceSignatureDao.", e);
}
}
private IDataSet getDataSet(String datasetLocation) throws Exception {
return new FlatXmlDataSetBuilder()
.build(new FileInputStream(datasetLocation));
}
private void cleanlyInsert(IDataSet dataSet) throws Exception {
databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTester.setDataSet(dataSet);
databaseTester.onSetup();
}
private void clean(IDataSet dataSet) throws Exception {
databaseTester.setSetUpOperation(DatabaseOperation.DELETE_ALL);
databaseTester.setDataSet(dataSet);
databaseTester.onSetup();
}
}
To use the hibernate-testing library, we need to add the following Maven dependency:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-testing</artifactId>
<version>${hibernate.version}</version>
<scope>test</scope>
<!-- Sometimes, we need to add this exclusion to get rid of
the following problem: Could not find artifact com.sun:tools:jar:1.6 -->
<exclusions>
<exclusion>
<artifactId>tools</artifactId>
<groupId>com.sun</groupId>
</exclusion>
</exclusions>
</dependency>
This way, we have got a light-weight integration test, which runs very fast and can also be used for the development and debugging of JPQL/SQL queries.
Opinions expressed by DZone contributors are their own.
Comments