Pragmatic integration testing can increase your productivity and ensure the deployability of your Java EE 6 application.
Introduction
In my previous article , “Unit Testing for Java EE”, I covered unit testing of Java Platform, Enterprise Edition 6 (Java EE 6) applications by mocking out all external dependencies with Mockito. Unit tests are important for the validation of application business logic, but they do not ensure the deployability of your Java EE 6 application.
Separation of (Performance) Concerns
Unit tests are fast and fine-grained. Integration tests are slow and coarse-grained.
Instead of using arbitrary categorizations for unit and integration tests, such as “slow” or “fast,” to improve productivity, we could rely on their natural properties. Unit tests are finer-grained, so they should run first. You are usually writing small chunks of functionality, before the functionality is integrated into a larger subsystem. Unit tests are extremely fast. Hundreds of unit tests can be executed in milliseconds. You can iterate faster with unit tests, without waiting for the completion of integration tests.
Integration tests are performed after a successful execution of unit tests. Integration tests are, therefore, executed less frequently, because unit tests will naturally fail often. With this strict separation between unit tests and integration tests, we can save several minutes (sometimes, hours) with each turnaround.
Testing for Productivity
Pragmatic integration testing does increase your productivity. The low-hanging fruit is testing of Java Persistence API (JPA) mapping and queries. Deploying the whole application to the server just to test the syntactical correctness of mapping and queries takes too long.
Fortunately JPA persistence can be started directly in the unit test. The bootstrapping overhead is negligible. You only have to create the EntityManager with the EntityManagerFactory. For a mapping test of the Prediction entity, a real EntityManager instance is injected into the PredictionAudit class (see Listing 1).
public class PredictionAuditIT {
private PredictionAudit cut;
private EntityTransaction transaction;
@Before
public void initializeDependencies(){
cut = new PredictionAudit();
cut.em = Persistence.createEntityManagerFactory("integration").createEntityManager();
this.transaction = cut.em.getTransaction();
}
@Test
public void savingSuccessfulPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, true);
transaction.begin();
this.cut.onSuccessfulPrediction(expectedResult);
transaction.commit();
List<Prediction> allPredictions = this.cut.allPredictions();
assertNotNull(allPredictions); assertThat(allPredictions.size(),is(1));
}
@Test
public void savingRolledBackPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, false);
this.cut.onFailedPrediction(expectedResult);
}
}
Listing 1: Injection of Unmanaged EntityManager
Since the EntityManager runs outside the container, the transactions can be managed only by the unit tests. Declarative transactions are not available in this case. This further simplifies the testing, because the transaction boundary can be explicitly set inside a test method. You can easily flush the EntityManager cache with an explicit EntityTransaction#commit() call. Right after the flush, the data is available in the database and can be validated for test purposes (see method savingSuccessfulPrediction in Listing 1).
Standalone JPA Configuration
EntityManager is part of the JPA specification and was already included with the glassfish-embedded-all dependency. Fortunately, the same dependency comes with the EclipseLink implementation. You need only an external database to persist the data. Derby database doesn’t require any installation and can be used in the server, embedded, and in an in-memory mode.
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>10.7.1.1</version>
<scope>test</scope>
</dependency>
Listing 2: “Installing” the Derby Database
Derby is maintained in the standard Maven repository and can be included with a single dependency (see Listing 2). Test-scoped dependency is used, because the Java DataBase Connectivity (JDBC) driver is needed only at test execution time, and it doesn’t have to be deployed or installed on the server.
Because we are going to start the unit test outside the container, we cannot rely on the availability of Java Transaction API (JTA) transactions and javax.sql.DataSource.
<persistence version=“1.0” 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_1_0.xsd">
<persistence-unit name="integration" transaction-type="RESOURCE_LOCAL">
<class>com.abien.testing.oracle.entity.Prediction</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:derby:memory:testDB;create=true"/>
<property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
<property name="eclipselink.ddl-generation" value="create-tables"/>
</properties>
</persistence-unit>
</persistence>
Listing 3: Unit Test-Specific persistence.xml Configuration
An additional, dedicated persistence.xml is created in the src/test/java/META-INF package and is used for testing exclusively. Because there is no deployment process, all entities have to be listed explicitly. Also transaction-type is set to RESOURCE_LOCAL, which enables manual transaction handling. Instead of a datasource declaration, the EntityManager talks directly to the database via the configured JDBC driver.
The embedded Derby database configuration is the most convenient for unit testing. The EmbeddedDriver supports two connection strings: file persistence and in-memory persistence. For JPA mapping and query testing, the in-memory connection string (see Listing 3) is the way to go. All tables are created on-the-fly in memory before each test, and they vanish after the test execution. Because you don’t have to clean up your data after the test, this is the most convenient setup for JPA smoke tests.
More-complex JPA tests require a defined set of test data, which cannot be met with an in-memory setup. Derby database can also use files instead of memory to persist and load its data. You only have to modify the connection string for this purpose:
<property name="javax.persistence.jdbc.url" value="jdbc:derby:./sample;create=true”/>
In particular, tests requiring a predefined set of data can be conveniently performed with the file-persistence configuration. The populated database has to be copied into the project folder before the test execution and it has to be deleted afterwards. Hence, the database is deleted after each run. You don’t even have to worry about cleanup or any modifications.
Unit Test Is Not Integration Test
You probably already noticed the strange looking “IT” suffix in the PredictionAuditIT class. The suffix is used to separate unit tests from the integration tests. The standard Maven failsafe plug-in executes all classes ending with IT or ITCase or starting with IT, but these classes are ignored by the Maven Surefire plug-in and the JUnit tests. You only have to add the following dependency to separate integration tests from unit tests:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-failsafe-plugin</artifactId><version>2.7.1</version></plugin>
Listing 4: Failsafe Plug-in Configuration
Unit tests are executed during the standard mvn clean install execution, and they can also be explicitly started with mvn surefire:test. Integration tests can also be included in the Maven phases with the execution tag:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.7.1</version>
<executions>
<execution>
<id>integration-test</id>
<phase>integration-test</phase>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
Listing 5: Phase Registration of Failsafe Plug-in
With the execution tag, the Failsafe plug-in gets automatically executed with the mvn install, mvn verify, or mvn integration-test commands. Unit tests are executed first, and then the integration tests are executed.
A strict separation between integration tests and unit tests significantly speeds up the turnaround cycle. Hence, unit tests validate the functionality of a method with a mocked-out environment, and they are orders of magnitude faster than integration tests. You get instantaneous feedback after the execution of unit tests.
Only a successful execution of all unit tests triggers the naturally slower integration tests. Depending on the environment, you can also set up dedicated Continuous Integration jobs for unit tests and integration tests. The mvn clean install job triggers the mvn integration-tests job then. The job separation gives you even more flexibility; you can re-execute the jobs separately and get notifications about the progress of each job.
A Killer Use Case for Embedded Integration Tests
The vast majority of all tests can be performed without firing up the container. JPA persistence can be easily tested with a locally created EntityManager. Also, the business logic can be tested outside the container in a convenient and effective way. Usually, you will even mock out all dependent classes, services, and other plumbing.
Let’s assume we have to externalize two error messages and keep them in one place for easy maintenance. String instances are injectable and can be used for configuration purposes (see Listing 6).
@Inject
String javaIsDeadError;
@Inject
String noConsultantError;
//…
if(JAVA_IS_DEAD.equals(prediction)){
throw new IllegalStateException(this.javaIsDeadError);
}
//…
if(company.isUnsatisfied()){
throw new IllegalStateException(this.noConsultantError);
Listing 6: Externalization with String Injection
The class MessageProvider maintains the configuration and produces field-dependent values (see Listing 7).
@Singleton
public class MessageProvider {
public static final String NO_CONSULTANT_ERROR = "No consultant to ask!";
public static final String JAVA_IS_DEAD_MESSAGE = "Please perform a sanity / reality check";
private Map<String,String> defaults;
@PostConstruct
public void populateDefaults(){
this.defaults = new HashMap<String, String>(){{
put("javaIsDeadError", JAVA_IS_DEAD_MESSAGE);
put("noConsultantError", NO_CONSULTANT_ERROR);}};
}
@Produces
public String getString(InjectionPoint ip){
String key = ip.getMember().getName();
return defaults.get(key);
}
}
Listing 7: A Generic Configurator
If you look at MessageProvider (Listing 7), there is nothing left to unit test. You could mock out the InjectionPoint parameter or the fieldMap defaults to test the lookup. The only critical parts are the mechanics behind the dependency injection itself in the getString method. The extraction of the field name, the lookup, and the injection itself can be reasonably tested only with the injection target inside the container.
The injection target is the class OracleResource, which uses the injected values for exception creation. (See my previous article, “Unit Testing for Java EE.") You will have either to expose the injected values or extract the message from the exception to test the mechanism. You would not be able to test corner cases, for example, unconfigured fields, with this approach. A helper class built specifically for test purposes gives you more flexibility and simplifies the test significantly.
public class Configurable {
@Inject
private String shouldNotExist;
@Inject
private String javaIsDeadError;
public String getShouldNotExist() {
return shouldNotExist;
}
public String getJavaIsDeadError() {
return javaIsDeadError;
}
}
Listing 8: Integration Test Helper Class
The class Configurable (Listing 8) resides in the folder src/test/java and was specifically designed to simplify the integration tests for the MessageProvider class. The javaIsDeadError field is supposed to be injected, and shouldNotExist was not meant to be configured and should have the value null after the test.
Aliens May Help You
Arquillian is an interesting alien and an open source testing framework integrated as a JUnit TestRunner. You have a full control over which classes are deployed and injected. Arquillian executes JUnit tests and has full access to the contents of the src/test/java folder. Dedicated test-helper classes such as Configurable (in Listing 8) can be used to simplify testing without packaging them with the production code in src/main/java.
Arquillian consists of a runner and container part. The arquillian-junit runner executes tests and realizes dependency injection inside a test case.
<dependency><groupId>org.jboss.arquillian</groupId><artifactId>arquillian-junit</artifactId><version>1.0.0.Alpha5</version><scope>test</scope></dependency><dependency><groupId>org.jboss.arquillian.container</groupId><artifactId>arquillian-glassfish-embedded-3.1</artifactId><version>1.0.0.Alpha5</version><scope>test</scope></dependency>
Listing 9: Arquillian Maven 3 Configuration
The arquillian-glassfish-embedded-3.1 dependency integrates an application server implementation. You can use arquillian-glassfish-embedded-3.1 (also 3.0),
arquillian-jbossas-embedded-6 (and older versions), Tomcat, Weld, or Jetty (see Listing 9).
import org.jboss.shrinkwrap.api.*;import javax.inject.Inject;import org.jboss.arquillian.junit.Arquillian;import org.junit.*;import static org.junit.Assert.*;import static org.hamcrest.CoreMatchers.*;@RunWith(Arquillian.class)
public class MessageProviderIT {@Inject
MessageProvider messageProvider;@Inject
Configurable configurable;@Deployment
public static JavaArchive createArchiveAndDeploy() {return ShrinkWrap.create(JavaArchive.class, "configuration.jar").addClasses(MessageProvider.class, Configurable.class).addAsManifestResource(new ByteArrayAsset("<beans/>".getBytes()),ArchivePaths.create("beans.xml"));}@Test
public void injectionWithExistingConfiguration() {String expected = MessageProvider.JAVA_IS_DEAD_MESSAGE;String actual = configurable.getJavaIsDeadError();assertNotNull(actual);assertThat(actual,is(expected));}@Test
public void injectionWithMissingConfiguration(){String shouldNotExist = configurable.getShouldNotExist();assertNull(shouldNotExist);}
}
Listing 10: Embedded Integration Tests with Arquillian
After the configuration of dependencies, you can use Arquillian as the test runner. The unit tests are executed by Arquillian transparently. Maven, Ant, or even your IDE will just use Arquillian instead of the original test runner to execute the tests. Although this indirection is transparent to you, it allows Arquillian to inject the deployed Contexts and Dependency Injection (CDI) or Enterprise JavaBeans (EJB) components or other Java EE resources directly to your tests. Arquillian is only a thin layer above the application server implementation. It boots GlassFish, JBoss, or Tomcat behind the scenes and executes the tests on a “native” embedded application server. It uses, for example, Embedded GlassFish behind the scenes.
Arquillian requires you to deploy the archive first. In the method createArchiveAndDeploy from Listing 10, a JAR archive with the classesMessageProvider and Configurable and the empty beans.xml is created and deployed. The deployed classes can be directly injected to the unit test itself, which greatly simplifies the testing. The method injectionWithExistingConfiguration asks the test helper Configurable to return the javaIsDeadError and shouldNotExist values (see Listing 8). You can test your code as if your test would be executed inside the application server. It’s only an illusion and the opposite is true: The test starts the container.
Testing the Impossible
It is hard to predict what happens if the dependency Instance<Consultant> company is unsatisfied. It actually cannot happen in production, because always enough Consultant implementations are deployed. This makes this corner case untestable in an integration environment. With Arquillian, it is very easy to test unsatisfied and ambiguous dependencies by manipulating the deployment unit.
@RunWith(Arquillian.class)
public class OracleResourceIT {
@Inject
OracleResource cut;
@Deployment
public static JavaArchive createArchiveAndDeploy() {
return ShrinkWrap.create(JavaArchive.class, "oracle.jar").
addClasses(OracleResource.class,MessageProvider.class, Consultant.class).
addAsManifestResource(new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
@Test(expected=IllegalStateException.class)
public void predictFutureWithoutConsultants() throws Exception{
try {
cut.predictFutureOfJava();
} catch (EJBException e) {
throw e.getCausedByException();
}
}
}
Listing 11: Testing the Impossible
Only the absolutely necessary classes, OracleResource, MessageProvider, and Consultant, are deployed with the oracle.jar archive created in the method createArchiveAndDeploy in Listing 11. Although the OracleResource class also uses an Event to send the prediction Result (see Listing 12), we are ignoring the PredictionAudit event listener by not deploying it.
@Path("javafuture")
@Stateless
public class OracleResource {
@Inject
Instance<Consultant> company;
@Inject
Event<Result> eventListener;
@Inject
private String javaIsDeadError;
@Inject
private String noConsultantError;
//…business logic omitted
}
Listing 12: Required Dependencies of OracleResource
The Event is expected to be swallowed. The method predictFutureWithoutConsultants invokes the method OracleResource#predictFutureOfJava and expects an IllegalStateException caused in the precondition check checkConsultantAvailability (see Listing 13).
public String predictFutureOfJava(){
checkConsultantAvailability();
Consultant consultant = getConsultant();
Result prediction = consultant.predictFutureOfJava();
eventListener.fire(prediction);
if(JAVA_IS_DEAD.equals(prediction)){
throw new IllegalStateException(this.javaIsDeadError);
}
return prediction.name();
}
void checkConsultantAvailability(){
if(company.isUnsatisfied()){
throw new IllegalStateException(this.noConsultantError);
}
}
Listing 13: Precondition Check in OracleResource
Interestingly, javax.ejb.EJBException is thrown instead of the expected IllegalStateException (see Listing 14).
WARNING: A system exception occurred during an invocation on EJB OracleResource method
public java.lang.String com.abien.testing.oracle.boundary.OracleResource.predictFutureOfJava()
javax.ejb.EJBException
//omitting several lines of stacktrace…
at $Proxy126.predictFutureOfJava(Unknown Source) at com.abien.testing.oracle.boundary.__EJB31_Generated__OracleResource__Intf__Bean__.predictFutureOfJava(Unknown Source)
Listing 14: Injection Stack Trace with an EJB Proxy
The unit test accesses a real EJB 3.1 bean through its public interface. IllegalStateException is an unchecked exception, which causes the current transaction to roll back, and it is propagated as javax.ejb.EJBException. We have to unwrap the IllegalStateException from the EJBException and re-throw it during the test.
Testing the Rollback
The rollback behavior can also be easily tested with the introduction of another test helper: TransactionRollbackValidator. It is even an EJB 3.1 bean with access to the SessionContext.
@Stateless
public class TransactionRollbackValidator {
@Resource
SessionContext sc;
@EJB
OracleResource os;
public boolean isRollback(){
try {
os.predictFutureOfJava();
} catch (Exception e) {
//swallow all exceptions intentionally
}
return sc.getRollbackOnly();
}
}
Listing 15: A Test Helper EJB 3.1 Bean
The TransactionRollbackValidator test class invokes OracleResource, swallows all exceptions, and returns the current rollback status. We only have to slightly extend the deployment and test section of OracleResourceIT to test the rollback behavior as well (see Listing 16).
@RunWith(Arquillian.class)
public class OracleResourceIT {
//other injections omitted
@Inject
TransactionRollbackValidator validator;
@Deployment
public static JavaArchive createArchiveAndDeploy() {
return ShrinkWrap.create(JavaArchive.class, "oracle.jar").
addClasses(TransactionRollbackValidator.class,
OracleResource.class,MessageProvider.class, Consultant.class).
addAsManifestResource(new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
@Test
public void rollbackWithoutConsultants(){
assertTrue(validator.isRollback());
}
//already discussed unit test omitted
}
Listing 16: Testing the Rollback Behavior
TransactionRollbackValidator is an EJB 3.1 bean, so it is deployed with the Required transaction attribute as default. With this convention, the method TransactionRollbackValidator#isRollback is always executed in a transaction scope. Either a new transaction is started or the existing one is reused. In our test case, the TransactionRollbackValidator invokes in a new transaction the OracleResource EJB 3.1 bean.
The freshly started transaction is propagated to OracleResource, which throws the EJBExceptioncaused by the lack of consultants. TransactionRollbackValidator just returns the result of the SessionContext#getRollbackOnly invocation (see Listing 15).
Such a test would be not possible without modification of the production code or without introduction of additional helper classes into thesrc/main/java source folder. Frameworks such as Arquillian give you a unique opportunity to easily test the infrastructure without polluting your production code with test helpers.
…and Continuous Deployment?
Testing your code outside and inside the container ensures only the correct behavior of your application under well-defined circumstances. Even a small server configuration change or a forgotten resource would break the deployment. All modern application servers are either scriptable or configurable through a headless API.
Reconstructing the server from configuration maintained with your code not only minimizes potential errors, but also makes manual interactions superfluous. You could even bring your application into production on every push or commit. Solid integration and unit tests are the first prerequisite for “continuous deployment.”
Overuse Hurts Productivity
Testing everything inside an embedded container is convenient but unproductive. Java EE 6 components are annotated Plain Old Java Objects (POJOs) and can be easily unit tested and mocked out (see my previous article, “Unit Testing for Java EE.") with Java SE tools such as JUnit, TestNG, or Mockito. Using an embedded container to test your business logic is not only unproductive, but it is also conceptually wrong.
Unit tests should validate your business logic, not the container behavior. Also, the majority of integration tests could be easily performed with a local EntityManager (see Listing 1) or a mocked-out container infrastructure. Only a fraction of all integration tests can be beneficially implemented with embeddable containers or test frameworks such as Arquillian. Although such tests represents only a fraction of the overall integration test base, these tests represent the ultimate killer use cases. Without an embeddable container, it is impossible to test infrastructure-dependent business logic without changing the integration environment or polluting the production code with test helpers.
See Also
- Arquillian (alien)
- Arquillian (framework)
- Mockito
- Embedded GlassFish
- Maven
- Derby database:
- Real World Java EE Night Hacks—Dissecting the Business Tier (see “Injection and Infrastructure Testing with Aliens” on page 110)
No comments :
Post a Comment