Pragmatic integration testing can improve your productivity and ensure the deployment of a JavaEE application.Adam Bean (Germany) is a consultant, trainer, architect, member of the Java EE 6 and 7 expert group, EJB 3.X, JAX-RS, and JPA 2.X JSRs. Java Champion, Top Java Ambassador 2012 and JavaOne Rock Star 2009, 2011, 2012 and 2013. Author of the books “Real World Java EE Patterns – Rethinking Best Practices” and “Real World Java EE Night Hacks – Dissecting the Business Tier”.
We invite you to familiarize yourself with the translation of Adam's article "
Integration Testing for Java EE "
.
Introduction
In a previous article,
Unit Testing for Java EE , I reviewed the unit testing approaches for Java applications and applications written using Java EE 6 by simulating all external dependencies using the Mokito library. Unit tests are important for validating business logic, but do not guarantee the deployment of your Java EE 6 application.
Note: On Java.Net, you will find the Maven 3 project for this article (TestingEJBAndCDI), which was tested using NetBeans 7 and GlassFish v3.x.
')
Note: On Java.Net, you will find the Maven 3 project for this article (
TestingEJBAndCDI ), which was tested using NetBeans 7 and GlassFish v3.x.
Using different approaches in testing
Unit tests - fast and "fine-grained". Integration tests are slow and coarse-grained. Instead of using arbitrary division of modular and integration tests into fast and slow, respectively, we’ll take their specifics into account to improve performance. Fine grain tests should be performed quickly. Typically, tests are written for small parts of the functional, before they are integrated into a larger subsystem. Unit tests are incredibly fast - hundreds of tests can be run in milliseconds. Using unit tests, it is possible to perform fast iterations and not wait for the integration tests to be completed.
Integration tests are performed after successfully passing unit tests. Unit tests often do not work, so integration tests are run less frequently. Due to strict division into modular and integration tests, it is possible to save a few minutes (or even hours) on each test cycle.
Performance Testing
Pragmatic integration testing will really increase your productivity. Another thing is testing mappings and Java Persistence API (JPA) requests. Downloading the entire application to the server only to check the correctness of the syntax of mappings and requests takes too much time.
JPA can be used directly from the unit test. Then the cost of restarting will be insignificant. To do this, you just need to get an instance of EntityManager from the EntityManagerFactory. For example, to test the mapping of the Prediction class, the EntityManager is embedded in the PredictionAudit class (see Example 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); } }
Example 1. Embedding EntityManager in the PredictionAudit classAs in Example 1 the EntityManager works outside the container, transactions can only be managed by unit tests. Declarative transactions are not available in this case. This makes testing even easier, since the transaction boundary can be explicitly set inside the test method. You can easily clear the EntityManager cache by calling the EntityTransaction # commit () method. Immediately after clearing the cache, data becomes available in the database and can be checked during the testing process (see the savingSuccessfulPrediction () method in Example 1).
Standalone JPA configurations
EntityManager is part of the JPA specification, and was also included in glassfish-embedded-all-dependency. The same dependency is performed in EclipseLink. You only need an external database to store data. Derby database does not require installation, it can run in server mode or in embedded database mode (embedded) and can store data in in-memory or on disk.
dependency> <groupId>org.apache.derby</groupId> <artifactId>derbyclient</artifactId> <version>10.7.1.1</version> <scope>test</scope> </dependency>
Example 2. “Installing” the Derby databaseDerby is supported by the standard Maven repository and can be added to the project as a single dependency (see example 2). In this case, the dependency is defined for the test scope, since the JDBC driver is only needed during testing and should not be deployed or installed on the server.
When conducting unit testing without a container, it is impossible to use
Java Transaction (JTA) functionality and
javax.sql.DataSource .
<persistence version=“1.0” xmlns="#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="# "> <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>
Example 3. A persistence file configured for unit testing.An additional persistence.xml file is created in the
src / test / java / META-INF package and is used exclusively for testing. Since there is no deployment process, all entities have to be explicitly described. Also, the transaction type is set to RESOURCE_LOCAL, which allows you to process transactions manually. Instead of declaring a data source, EntityManager accesses the database directly through a configured JDBC driver. Derby database in embedded mode is best suited for unit testing. EmbeddedDriver supports two URL configuration options: storing data in a file or in memory. For testing JPA-mapping and queries, the in-memory connection string is used (in-memory, see example 3). All tables are created on the fly in RAM before the next test and are deleted after the test. Since there is no need to delete your data after testing, this is the most convenient way to smoke test JPA.
More complex JPA tests require a specific set of test data, and using the in-memory configuration for such cases is inconvenient. Derby database instead of RAM can use files to store and load data. To do this, just change the connection string:
<property name="javax.persistence.jdbc.url" value="jdbc:derby:./sample;create=true”/>
In particular, tests that require a predefined dataset can be easily implemented using a database configuration with state recording to a file. The completed database will have to be copied to the project folder before the test is executed, and it must be deleted after testing. Thus, the database will be deleted after each launch. No need to worry about cleaning or any modifications.
The unit test is not an integration test.
You've probably already noticed a strange IT suffix in the name of the
PredictionAuditIT class. The suffix is ​​used to distinguish unit tests from integration tests. The standard failsafe Maven framework executes all tests ending in IT or ITCase, or starting with IT, but these classes are ignored by Maven with the Surefire plugin and JUnit tests. You just need to add the following dependency to separate the unit and integration tests:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.7.1</version> </plugin>
Example 4. FailSafe plugin configurationUnit tests are performed during the standard mvn clean install, and can also be explicitly started using the mvn surefire: test command. Integration tests can also be added to the Maven phases using 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>
Example 5. Registering the Failsafe pluginWhen using the
execution tag, the
Failsafe plugin starts automatically with the
mvn install ,
mvn verify, or
mvn integration-test commands. First, unit tests are performed, and then integration tests.
Strict division into integration and unit tests significantly speeds up the testing cycle. Thus, unit tests test the functionality of a method in a simulated environment, and they are several orders of magnitude faster than integration tests. Instant result is obtained after the execution of unit tests.
Only the successful completion of all unit tests starts the initially slower integration tests. It is also possible to configure Continuous Integration separately for unit testing and integration testing. The
mvn clean install command launches the
mvn integration-tests command. Separating processes gives you more flexibility; You can restart the tasks separately and receive notifications about the progress of each task.
Killer use case for embedded integration tests
Most integration tests can be performed without running the container. JPA functionality can be tested using an
EntityManager created locally. Business logic can also be conveniently and efficiently tested outside the container. For it is necessary to create mock-objects for all dependent classes, services and everything else.
Let's assume that we need to output two error messages and keep them in one place for the convenience of support. Instances of the String class can be embedded and used for configuration (see Example 6).
@Inject String javaIsDeadError; @Inject String noConsultantError;
Example 6. Throwing exceptions using embedded stringsThe
MessageProvider class supports configuration and returns a string using property values ​​(see Example 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); } }
Example 7. General configuratorIf you look at the
MessageProvider class (example 7), you can see that there is nothing left to be tested. Is it possible to create a mock object for the
InjectionPoint parameter of the
Map defaults field to test the search. The only untested part after
dependency injection remains
dependency injection itself and using them in the
getString () method. Obtaining the field name, the search and the implementation itself can be fully tested only inside the container. The implementation target is the
OracleResource class, which uses embedded values ​​to create exceptions. (See the article "
Unit Testing for Java EE .") In order to test this mechanism, you will have to either display the embedded values, or extract the message from the exceptions. With this approach, it will not be possible to test "critical moments", for example, unconfigured fields. An auxiliary class created solely for testing purposes will give more flexibility and greatly simplify testing.
public class Configurable { @Inject private String shouldNotExist; @Inject private String javaIsDeadError; public String getShouldNotExist() { return shouldNotExist; } public String getJavaIsDeadError() { return javaIsDeadError; } }
Example 8. Auxiliary class for integration testingThe
Configurable class (see Example 8) is located in the
src / test / java folder and was specifically designed to simplify integration testing for the
MessageProvider class. It is assumed that the
javaIsDeadError field will be embedded, and the
shouldNotExist field
will not need to be configured, and during testing it will be
null .
"Aliens" can help you
Arquillian is an interesting creature of unearthly origin (see the film “Men in Black”), but at the same time it is also an open source framework integrated into the TestRunner class of the JUnit framework. With it, you have the opportunity of complete control over which classes are deployed and implemented. Arquillian performs JUnit tests and has full access to the contents of the
src / test / java folder. Special classes for testing, such as
Configurable (see Example 8), can be used to simplify testing, while the tests will not fall into the folder with the main application code src / main / java, and there is no need to include them in the delivery.
Arquillian consists of two parts: the control part and the container. The control part of
arquillian-junit performs tests and implements the dependency injection mechanism inside the 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>
Example 9. Arquillian configuration for Maven 3The dependency
arquillian-glassfish-embedded-3.1 integrates with the application server. You can use both version 3.1 and 3.0.
Instead of
glassfish, you can use the
JBoss embedded server using the
arquillian-jbossas-embedded-6 artifact, or Tomcat and Weld servers.
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); }
Example 10. Integration tests using ArquillianAfter configuring dependencies, Arquillian can be used as a test management system. Unit tests are performed transparently in Arquillian. Maven, Ant or even your IDE will simply use Arquillian instead of the native test execution system. Although this redirection is transparent, it allows Arquillian to deploy deployed Contexts and Dependency Injection (CDI) or Enterprise JavaBeans (EJB) components, or other Java EE resources directly into your tests. Arquillian is just a thin layer above the application server implementation. It silently loads GlassFish, JBoss or Tomcat and runs tests on a native embedded application server. For example, it can use Embedded GlassFish.
To start using Arquillian, we need to close the JAR archive. In the
createArchiveAndDeploy method (see Example 10), a JAR is created and deployed with the
MessageProvider and
Configurable classes and with an empty beans.xml file. Deployed classes can be directly embedded in the unit test itself, which greatly simplifies testing. The
injectionWithExistingConfiguration method refers to the
Configurable class, which returns the
javaIsDeadError and
shouldNotExist values (see Example 8). You can test the code as if it were running inside an application server. On the one hand, this is only an illusion, on the other hand, no, because the tests actually launch the container.
Testing the impossible
It is difficult to predict what will happen if the dependence of
Instance Consultant> company is not satisfied. This cannot happen in actual operation, since at this time there are always enough implementations of the
Consultant class. This situation makes testing impossible in an integration environment. But Arquillian makes it easy to test unsatisfied and undefined dependencies by controlling the deployment module.
@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(); } } }
Example 11. Testing the impossibleOnly the most needed
OracleResource ,
MessageProvider, and
Consultant classes are placed in the
oracle.jar archive using
createArchiveAndDeploy from Example 11. Although the
OracleResource class also uses
Event to send a Result (see Example 12), we ignore the
PredictionAudit event and do not deploy it.
@Path("javafuture") @Stateless public class OracleResource { @Inject Instance<Consultant> company; @Inject Event<Result> eventListener; @Inject private String javaIsDeadError; @Inject private String noConsultantError;
Example 12. Required dependencies of the OracleResource class.We assume that the event
Event will be swallowed. The
predictFutureWithoutConsultants method calls the
OracleResource # predictFutureOfJava method and waits for an
IllegalStateException thrown after checking the condition in
checkConsultantAvailability (see Example 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); } }
Example 13. OracleResource precondition checkNote that a
javax.ejb.EJBException is thrown instead of the expected
IllegalStateException (see Example 14).
WARNING: A system exception occurred during a call on the EJB side.
class OracleResource public java.lang.String com.abien.testing.oracle.boundary.OracleResource.predictFutureOfJava() javax.ejb.EJBException // stacktrace… at $Proxy126.predictFutureOfJava(Unknown Source) at com.abien.testing.oracle.boundary.__EJB31_Generated__OracleResource__ __Intf____Bean__.predictFutureOfJava(Unknown Source)
Example 14. Implementing the Stack Trace class using an EJB Proxy.The unit test accesses a real EJB 3.1 component through a
public interface.
IllegalStateException is an unchecked exception that causes the current transaction to be
rolled back, and its action is wrapped in a
javax.ejb.EJBException exception. You have to get an
IllegalStateException exception from an
EJBException and re-throw it.
Rollback testing
Behavior in the event of a rollback of a transaction can be easily tested by introducing an auxiliary class:
TransactionRollbackValidator . This is a regular EJB 3.1 component with access to
SessionContext .
@Stateless public class TransactionRollbackValidator { @Resource SessionContext sc; @EJB OracleResource os; public boolean isRollback(){ try { os.predictFutureOfJava(); } catch (Exception e) {
Example 15. EJB 3.1 components for testingThe
TransactionRollbackValidator test class calls the
OracleResource class, handles all exceptions and returns the current rollback status of the transaction. You only have to slightly expand the deployment and testing section of the
OracleResourceIT class to verify the success of a transaction rollback (see Example 16).
@RunWith(Arquillian.class) public class OracleResourceIT {
Example 16. We test transaction rollbackTransactionRollbackValidator is a component of EJB 3.1, so it is deployed by default using the transaction attribute
Required . According to the EJB specification, the
TransactionRollbackValidator # isRollback method is always executed as part of a transaction. Either a new transaction is started, or an existing one is reused. In our test case,
TransactionRollbackValidator invokes a new transaction class
OracleResource . The newly launched transaction extends to the
OracleResource class, which throws an
EJBException due to the lack of consultants. The
TransactionRollbackValidator class simply returns the result of calling the
SessionContext # getRollbackOnly method .
Such a test would be impossible without modifying the source code or introducing additional auxiliary classes into the
src / main / java folder. Such frameworks as Arquillian provide a unique opportunity to easily test the infrastructure without littering the source code with the code for testing.
... and continuous deployment?
Testing the code outside and inside the container guarantees the correct behavior of your application only under well-defined working conditions. Even a slight server configuration change or a forgotten resource will break the deployment. All modern application servers are configured either through scripts or through a stripped-down API. Running the server from the configuration in your source code not only minimizes possible errors, but also makes manual intervention unnecessary. You can even deploy the application to industrial servers whenever you change the code in the repository. Comprehensive integration and modular tests are the first prerequisite for "continuous deployment."
Abuse harms performance
Testing all the functionality inside the embedded container is convenient but unproductive. Java EE 6 components are annotated Plain Old Java Objects (POJOs) and can be easily tested and mocked using Java SE tools such as JUnit, TestNG or Mockito. Using the built-in container to check the business logic is not only unproductive, but also conceptually wrong. Unit tests should check only the business logic, not the behavior of the container. In addition, most integration tests can be easily launched using a local EntityManager (see Example 1) or by imitating container infrastructure. For only a small part of integration tests, you should use embedded containers or test frameworks, such as Arquillian. – , . -, , .
17 19 JavaOne Rock Star «, , Java EE 7»