13
Mar
16

Development and Unit testing of Java EE / JPA applications


This page describes how to create a working environment that allows you to develop, run and test a Java JPA application. Except for the logging API, this example contains no vendor specific code. No Spring framework, no Hibernate, it uses pure java annotations wherever possible. It will use the OpenEJB/OpenJPA to provide the runtime environment to JUnit for testing.

This page is influenced by the concept described in book “Domain Driven Design” by Eric J. Evans. The example will use Java Session beans transaction and service Layer, repository beans with JPA Entity managers for data access and CDI for dependency injection, finally HSQLDB will store the results to the file-system for later analysis.

Once complete and tested this components could be used as part of a larger enterprise application that runs in a full JEE container with little to no modifications.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

Background

Java EE containers are large and clumsy to work with. They often times require you to package your application into WAR or EAR files and deploy them into a managed folder where they pick up and deploy internally within the container.

There is a need for a lighter weight solution. The solution should be quick to setup and run and allow for some level of JUnit testing.

Session beans on the other hand often times contain business logic that needs to be tested.

Mocking frameworks like JMock or Mockito allow to mock the data access layer. It is the preferred approach. The data is setup could be time consuming and awkward to setup in java code.

This page discusses a way to test the service and repository layer without mocking any classes, or hand coding of any table data in java code. Instead you can use JPA and HSQLDB to create and initialize a in-memory database.

Mocking frameworks can still be used to mock other aspects of the application that is not available to the service layer.

Software Stack

  1. Java 8
  2. Java EE 6
  3. JPA 2
  4. OpenEBJ 4.7

Test Case

The following is the main test case. It contains a reference to an EJB container that will be used to run the application. The produceContainer method accepts a name of a database. This will result in a file being created in the users $HOME/data/ directory. Each test case class can have a dedicated database instance. Once the test case is complete HSQL can write the results back to the file system or not. This functionality is customizable by passing properties in the connection URL for hsqldb.

src/test/java/org/test/UserManagerServiceTest.java

package org.test;

import javax.ejb.embeddable.EJBContainer;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class UserManagerServiceTest {
	private static EJBContainer container;
	
	@BeforeClass
	public static void beforeClass() throws Exception {
		// the following represents a db file in the users $HOME/data directory
	    container = ContainerProducer.produceContainer("usermanager_test1"); 
	}
	
	@AfterClass
	public static void afterClass() throws Exception {
		if(container!=null)
			container.close();
	}

	/**
	 * Use this method to cleanup and initialize the data before each test method invocation.
	 */
	@Before
	public void before() {}

	/**
	 * Use this method to cleanup the data after each test method invocation.
	 */
	@After
	public void after() {}
	
	@Test
	public void testAddUser() throws Exception {
	    UserManagerService userManagerService = (UserManagerService) container.getContext().lookup("java:global/UserManagerApp/UserManagerService");
	    int userCount = userManagerService.getUserCount();
	    String id = "user" + System.currentTimeMillis();
	    String name = "Test User" + System.currentTimeMillis();
	    String password = ""+System.currentTimeMillis();
		userManagerService.addUser(new User(id, name, password));
		int newUserCount = userManagerService.getUserCount();
		Assert.assertTrue(newUserCount > userCount);
	}
}

Container Producer

This is where the database connections are configured. The static method accepts a database name and saves an instance of the database into the users $HOME/data directory. The database is initialized with a completely blank schema. Once the test is complete the user can open the resulting database “.script” file and insert the mock data. Next time the test case runs it will load the data specified in the “.script” file. The JPA implementation will also check for any changes in the entity beans and propagate the DDL changes to the script file.

Since the scripts are regular files Using this method the developer has flexibility. For example the developer can have a complete library of script files one for each test case. The script files could also be checked into SVN or GIT version control so they can be shared or after finding a bug they could be restored to their original condition etc…

src/test/java/org/test/ContainerProducer.java

package org.test;

import java.util.HashMap;
import java.util.Map;

import javax.ejb.embeddable.EJBContainer;

public class ContainerProducer {

	public static final EJBContainer produceContainer(String dbName) {
	    // Create the container instance, passing it the properties map:
	    Map<String, Object> p = new HashMap<>();
	    String userHome = System.getProperty("user.home");
        p.put("DefaultDS", "new://Resource?type=DataSource");
        p.put("DefaultDS.JdbcUrl", "jdbc:hsqldb:file:"+userHome+"/data/"+dbName+";shutdown=true");

	    return javax.ejb.embeddable.EJBContainer.createEJBContainer(p);
	}
}

User Entity Class

The following represents a user object we will store to the database.

src/main/java/org/test/User.java

package org.test;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;

@Entity
@NamedQuery(name="getCount", query = "SELECT COUNT(u) from User u")
public class User {
	@Id
	@Column(length=50, nullable=false)
	private String id;
	@Column(length=50, nullable=false)
	private String name;
	@Column(length=25, nullable=false)
	private String password;
	
	public User(){}
	public User(String id, String name, String password) {
		this.id = id;
		this.name = name;
		this.password = password;
	}
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

Service Layer

Embarrassingly simple service layer.

src/main/java/org/test/UserManagerService.java

package org.test;

import javax.ejb.Stateless;
import javax.inject.Inject;

import org.slf4j.Logger;

@Stateless
public class UserManagerService {
	@Inject
	private Logger logger;

	@Inject
	private UserDataManager userDataManager;

	public void addUser(User user) throws Exception {
		logger.info("inside adduser");
		userDataManager.addUser(user);
	}

	public int getUserCount() {
		return userDataManager.getUserCount();
	}
}

Data Manager Layer

Embarrassingly simple data manager layer.

src/main/java/org/test/UserDataManager.java

package org.test;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.slf4j.Logger;

public class UserDataManager {
    @Inject
    private Logger logger;
    
	@PersistenceContext(unitName="UserManager")
	private EntityManager em;

	public void addUser(User user) {
		logger.info("inside addUser()");
		em.persist(user);
	}

	public int getUserCount() {
		return em.createNamedQuery("getCount", Long.class).getSingleResult().intValue();
	}
}

Logger CDI injection

Just a simple way to inject a logger into the entity classes.
src/main/java/org/test/LoggerProducer.java

package org.test;
 
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggerProducer {
    @Produces
    public Logger produceLogger(InjectionPoint injectionPoint) {
    	// check log4j.xml to find out where logs are being written to.
    	return LoggerFactory.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
    }
}

Project Configuration

src/main/resources/log4j.xml

<!DOCTYPE log4j:configuration PUBLIC
  "-//APACHE//DTD LOG4J 1.2//EN" "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
   
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    
	<appender name="file" class="org.apache.log4j.FileAppender">
	   <param name="file" value="${user.home}/logs/UserManager.log" />
	   <layout class="org.apache.log4j.PatternLayout">
		<param name="ConversionPattern" 
			value="%d %-5p %c:%L - %m%n" />
	   </layout>
	</appender>
	    
    <logger name="org.test">
        <level value="DEBUG"/>
    </logger>
    <logger name="org">
        <level value="DEBUG"/>
    </logger>
    <root>
        <level value="ERROR"/>
        <appender-ref ref="file"/>
    </root> 
</log4j:configuration>

The following file should in the META-INF/ directory. If its going to be part of a war it should be located in a jar file that is inside the WEB-INF/lib directory. Read your containers documentation on the proper location of this file if its not clear.

JPA Config

src/main/resources/META-INF/persistence.xml

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

	<persistence-unit name="UserManager">
		<provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
		<jta-data-source>DefaultDS</jta-data-source>

		<properties>
			<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)" />
		</properties>
	</persistence-unit>
</persistence>

CDI config

Create a blank src/main/resources/META-INF/beans.xml file to enable Java contexts and dependency injection (CDI)

Maven Configuration

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.test</groupId>
	<artifactId>UserManagerApp</artifactId>
	<version>20160313</version>
	<packaging>jar</packaging>

	<name>UserManager</name>
	<pluginRepositories>
		<pluginRepository> <!-- Ignore this repository. Its only used for document publication. -->
			<id>numberformat-releases</id>
			<url>https://raw.githubusercontent.com/numberformat/wordpress/master/20130213/repo/</url>
		</pluginRepository>
	</pluginRepositories>


	<dependencies>
		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.1</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.1</version>
		</dependency>
		<!-- Unit Testing -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.openejb</groupId>
			<artifactId>openejb-core</artifactId>
			<version>4.7.1</version>
			<scope>test</scope>
		</dependency>

		<!-- APIs for services provided by the containers -->
		<dependency>
			<groupId>javax</groupId>
			<artifactId>javaee-web-api</artifactId>
			<version>6.0</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin> <!-- Ignore this plugin. Its only used for document publication. -->
				<groupId>github.numberformat</groupId>
				<artifactId>blog-plugin</artifactId>
				<version>1.0-SNAPSHOT</version>
				<configuration>
					<gitUrl>https://github.com/numberformat/wordpress/tree/master/${project.version}/${project.artifactId}</gitUrl>
				</configuration>
				<executions>
					<execution>
						<id>1</id>
						<phase>site</phase>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<!-- this configures the surefire plugin to run your tests with the javaagent 
				enabled -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<configuration>
					<forkMode>pertest</forkMode>
					<argLine>-javaagent:${project.basedir}/target/openejb-javaagent-4.6.0.jar</argLine>
					<workingDirectory>${project.basedir}/target</workingDirectory>
				</configuration>
			</plugin>
			<!-- this tells maven to copy the openejb-javaagent jar into your target/ 
				directory -->
			<!-- where surefire can see it 
			Eclipse JUnits can use the following VM arg: -javaagent:target/openejb-javaagent-4.6.0.jar -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<executions>
					<execution>
						<id>copy</id>
						<phase>process-resources</phase>
						<goals>
							<goal>copy</goal>
						</goals>
						<configuration>
							<artifactItems>
								<artifactItem>
									<groupId>org.apache.openejb</groupId>
									<artifactId>openejb-javaagent</artifactId>
									<version>4.6.0</version>
									<outputDirectory>${project.build.directory}</outputDirectory>
								</artifactItem>
							</artifactItems>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

Run the test case

The test case should run with no problems.

Database will go to the users $HOME/data directory. Logs will go to $HOME/logs.

Advertisements

0 Responses to “Development and Unit testing of Java EE / JPA applications”



  1. Leave a Comment

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 75 other followers

March 2016
S M T W T F S
« Feb   Mar »
 12345
6789101112
13141516171819
20212223242526
2728293031  

Blog Stats

  • 806,836 hits

%d bloggers like this: