Think testing Java EE applications is difficult, inconvenient, or too complex? Learn why this isn't the case and how do unit testing efficiently in this article.
@Column(name="prediction_result")
@Enumerated(EnumType.STRING)
private Result result;
@Temporal(TemporalType.TIME)
private Date predictionDate;
Testing is one of the remaining mysterious areas of Java Platform, Enterprise Edition (Java EE). It is a common misconception that testing Java EE applications is hard, inconvenient, or too complex. This has not been true since the inception of Java EE 5 more than five years ago. In this article, I delve into unit testing. In a subsequent article, I will explore integration and embedded container tests.
Ask an Oracle About the Future of Java
I will use an “oracle” application that is able to predict the future of Java by using some consultants behind the scenes. For clarification, an “oracle” is defined as follows:
“In Classical Antiquity, an oracle was a person or agency considered to be a source of wise counsel or prophetic opinion, predictions or precognition of the future, inspired by the gods. As such it is a form of divination.” [http://en.wikipedia.org/wiki/Oracle].
In modern times, even an oracle would rely on representational state transfer (REST) to expose his predictions (see Listing 1). OracleResource is an Enterprise JavaBeans (EJB) 3.1 bean, which is exposed via Java API for RESTful Web Services (JAX-RS) as a REST resource and gateway to Contexts and Dependency Injection (CDI) managed beans. OracleResource maintains an injected pool of consultants (Instance<Consultant> company) and asks the first consultant to predict the future of Java.
@Path("javafuture")
@Stateless
@Stateless
public class OracleResource {
@Inject
Instance<Consultant> company;
@Inject
Event<Result> eventListener;
@GET
@Produces(MediaType.TEXT_PLAIN)
@Inject
Instance<Consultant> company;
@Inject
Event<Result> eventListener;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String predictFutureOfJava(){
checkConsultantAvailability();
Consultant consultant = getConsultant();
Result prediction = consultant.predictFutureOfJava();
eventListener.fire(prediction);
if(JAVA_IS_DEAD.equals(prediction))
throw new IllegalStateException("Please perform a sanity / reality check");
return prediction.name();
}
checkConsultantAvailability();
Consultant consultant = getConsultant();
Result prediction = consultant.predictFutureOfJava();
eventListener.fire(prediction);
if(JAVA_IS_DEAD.equals(prediction))
throw new IllegalStateException("Please perform a sanity / reality check");
return prediction.name();
}
void checkConsultantAvailability(){
if(company.isUnsatisfied())
throw new IllegalStateException("No consultant to ask!");
}
if(company.isUnsatisfied())
throw new IllegalStateException("No consultant to ask!");
}
Consultant getConsultant(){
for (Consultant consultant : company)
return consultant;
return null;
}
}
for (Consultant consultant : company)
return consultant;
return null;
}
}
Listing 1: EJB 3.1 Bean as a REST Resource and Gateway to CDI Managed Beans
Consultant is a Java interface implemented by a Blogger, ReasonableConsultant, and SmartConsultant. OracleResource just picks the first one and asks it about the future. All answers are accepted, except JAVA_IS_DEAD, which causes an IllegalStateException. Usually, you would use a javax.inject.Qualifier to disambiguate your choice, but a javax.enterprise.inject.Instance is harder to test, so I used that to make testing more “interesting.”
public class Blogger implements Consultant{
@Override
public Result predictFutureOfJava() {
return Result.JAVA_IS_DEAD;
}
}
@Override
public Result predictFutureOfJava() {
return Result.JAVA_IS_DEAD;
}
}
Listing 2: A Blogger Consultant Implementation
All predictions are distributed as transactional events. Each prediction is executed in an independent transaction started by the EJB container. It is the convention; no additional configuration is required for this purpose.
The event is received by a PredictionAudit EJB 3.1 bean, which persists all successful and failed predictions with Java Persistence API (JPA) 2, since some consultants are known for rolling back their decisions (see Listing 3).
@Stateless
public class PredictionAudit {
@PersistenceContext
EntityManager em;
public class PredictionAudit {
@PersistenceContext
EntityManager em;
public void onSuccessfulPrediction(@Observes(during= TransactionPhase.AFTER_SUCCESS)
Result result){
persistDecision(result,true);
}
Result result){
persistDecision(result,true);
}
public void onFailedPrediction(@Observes(during= TransactionPhase.AFTER_FAILURE)
Result result){
persistDecision(result,false);}
Result result){
persistDecision(result,false);}
void persistDecision(Result result,boolean success) {
Prediction prediction = new Prediction(result,success);
em.persist(prediction);}
Prediction prediction = new Prediction(result,success);
em.persist(prediction);}
public List<Prediction> allDecisions(){
return this.em.createNamedQuery(Prediction.findAll).getResultList();}
}
return this.em.createNamedQuery(Prediction.findAll).getResultList();}
}
Listing 3: Event-Driven PredictionAudit
The CDI event nicely decouples the OracleResource from the PredictionAudit, but it makes testing harder at the same time. The JPA 2 entity Prediction is persisted on each prediction, regardless of whether the transaction is committed or rolled back (see Listing 4).
@Entity
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@NamedQuery(name=Prediction.findAll,query="Select d from Prediction d")
public class Prediction {
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@NamedQuery(name=Prediction.findAll,query="Select d from Prediction d")
public class Prediction {
public final static String PREFIX = "com.abien.testing.oracle.entity.Prediction.";
public final static String findAll = PREFIX + "findAll";
public final static String findAll = PREFIX + "findAll";
@Id
@GeneratedValue
@XmlTransient
private long id;
@GeneratedValue
@XmlTransient
private long id;
@Column(name="prediction_result")
@Enumerated(EnumType.STRING)
private Result result;
@Temporal(TemporalType.TIME)
private Date predictionDate;
private boolean success;
public Prediction() {
this.predictionDate = new Date();
}
this.predictionDate = new Date();
}
public Prediction(Result result, boolean success) {
this();
this.result = result;
this.success = success;
}
this();
this.result = result;
this.success = success;
}
//bookkeeping methods omitted
}
}
Listing 4: JPA 2 Entity Decision
Maven 3 and Unit Testing—Before You Start
There is nothing special about unit testing Java EE 6 applications. You only have to add the JUnit library into your pom.xml file (see Listing 5) and put your classes into the src/test/java directory. All JUnit tests will be executed automatically during the standard Maven lifecycle: mvn clean install.
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
Listing 5: JUnit Library Inclusion in pom.xml
You will get a strange error by loading the Java EE 6 API classes included by the standard Maven archetype (see Listing 6).
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
Listing 6: Unusable Reference to API
The standard Java EE 6 APIs in the Maven repository were processed by a tool, which removes the method body implementation from the bytecode and makes the javaee-web-api dependency unusable for unit testing. Any attempt to load the classes from the Java EE 6 API leads to an error like the following:
Absent Code attribute in method that is not native or abstract in class file javax/enterprise/util/TypeLiteral
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/enterprise/util/TypeLiteral
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/enterprise/util/TypeLiteral
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
Listing 7: Error Caused by Loading a Class from Java EE 6 API
You should replace the Java EE 6 API classes by the application vendor implementation. For GlassFish v3.1, the most convenient way is to use the single dependency on the embedded container:
<dependency>
<groupId>org.glassfish.extras</groupId>
<artifactId>glassfish-embedded-all</artifactId>
<version>3.1</version>
<scope>provided</scope>
</dependency>
<groupId>org.glassfish.extras</groupId>
<artifactId>glassfish-embedded-all</artifactId>
<version>3.1</version>
<scope>provided</scope>
</dependency>
Listing 8: Java EE 6 API Replacement
You could, of course, pick and choose the CDI, JPA, EJB, etc. libraries for JUnit testing or compilation, or you could use working alternatives from JBoss or Geronimo Maven mirrors.
Why Drink Mockito?
Mockito is an easy-to-use, open source mocking library. Mockito is able to create “smart proxies” (a.k.a. mocks) from classes or interfaces. These proxies do not come with any behavior, but they are still perfectly usable. You can invoke methods, but will get the default values, or null, back. The behavior of the mocks can be recorded after their creation with when(mock.getAnswer()).then(42) syntax.
Mockito is perfectly suitable for “simulating” any inconvenient classes, resources, or services. You can start with Mockito just by knowing a single class org.mockito.Mockito. The when-then “domain specific language” is composed of static methods from the Mockito class. The org.mockito.Mockito class is well documented. In fact, the whole documentation set was generated from the JavaDoc markup in the org.mockito.Mockito class.
Unit Testing Java EE 6 with Some Mockito
PredictionAudit is intended to set the “success” flag dependent on the transaction outcome and use the EntityManager to persist the Prediction entity. To test the PredictionAudit class precisely will have to “simulate” the EntityManager. We will test only whether the method EntityManager#persist receives the correct parameter, not whether the Prediction entity is actually persisted. (We will explore integration testing of the interaction between PredictionAudit and the actual JPA EntityManager functionality in a subsequent article.)
A mocked-out EntityManager instance is created with the static and generified mock(EntityManager.class) method.
import static org.mockito.Mockito.*;
public class PredictionAuditTest {
private PredictionAudit cut;
@Before
public void initializeDependencies(){
cut = new PredictionAudit();
cut.em = mock(EntityManager.class);}
private PredictionAudit cut;
@Before
public void initializeDependencies(){
cut = new PredictionAudit();
cut.em = mock(EntityManager.class);}
@Test
public void savingSuccessfulPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, true);
this.cut.onSuccessfulPrediction(expectedResult);
verify(cut.em,times(1)).persist(expected);}
public void savingSuccessfulPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, true);
this.cut.onSuccessfulPrediction(expectedResult);
verify(cut.em,times(1)).persist(expected);}
@Test
public void savingRolledBackPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, false);
this.cut.onFailedPrediction(expectedResult);
verify(cut.em,times(1)).persist(expected);}
}
public void savingRolledBackPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, false);
this.cut.onFailedPrediction(expectedResult);
verify(cut.em,times(1)).persist(expected);}
}
Listing 9: Unit Testing and Mocking of PredictionAudit
The mocked-out EntityManager instance is directly injected into the default visible field PredictionAudit#em. Within the test method savingSuccessfulPrediction (see Listing 9), a Result instance is created and passed to the onSuccessfulPrediction method, which creates a Prediction instance and invokes the EntityManager#persist (see Listing 3). The static method verify(cut.em,times(1)).persist(expected) verifies whether the EntityManager#persist method is invoked exactly one time with the expected parameters.
A More Complex Mocking Case
The EJB 3.1 OracleResource bean’s injection of the javax.enterprise.event.Event and javax.enterprise.inject.Instance instances is far more interesting to test. The initialization and mocking of CDI dependencies is performed again with the Mockito#mock method (see Listing 10).
public class OracleResourceTest {
private OracleResource cut;
@Before
public void initializeDependencies(){
this.cut = new OracleResource();
this.cut.company = mock(Instance.class);
this.cut.eventListener = mock(Event.class);
}
private OracleResource cut;
@Before
public void initializeDependencies(){
this.cut = new OracleResource();
this.cut.company = mock(Instance.class);
this.cut.eventListener = mock(Event.class);
}
Listing 10: Initialization and Mocking of CDI Dependencies
Let’s start with a test of the helper method checkConsultantAvailability verification of the consultant’s availability. We expect an IllegalStateException if no Consultant implementation was found. The static method Mockito#when records expected behavior for an instance returned by the Mockito#mock method. We just return true for the method Instance#isUnsatisfied and expect an IllegalStateException:
@Test(expected=IllegalStateException.class)
public void checkConsultantAvailabilityWithoutConsultant()
{
when(this.cut.company.isUnsatisfied()).thenReturn(true);
this.cut.checkConsultantAvailability();
}
public void checkConsultantAvailabilityWithoutConsultant()
{
when(this.cut.company.isUnsatisfied()).thenReturn(true);
this.cut.checkConsultantAvailability();
}
Listing 11: Prerecording Mock Behavior
The EJB bean OracleResource throws an IllegalStateException if a Consultant offers an insane prediction. The Blogger implementation of the Consultant interface always returns JAVA_IS_DEAD, which causes the IllegalStateException. To test that behavior, we have to mock out the Iterator returned by javax.enterprise.inject.Instance:
Iterator mockIterator(Consultant consultant) {
Iterator iterator = mock(Iterator.class);
when(iterator.next()).thenReturn(consultant);
when(iterator.hasNext()).thenReturn(true);
return iterator;
}
Iterator iterator = mock(Iterator.class);
when(iterator.next()).thenReturn(consultant);
when(iterator.hasNext()).thenReturn(true);
return iterator;
}
Listing 12: Mocking Out the Iterator
We change the behavior of Instance to return the mocked-out Iterator instance instead:
@Test(expected=IllegalStateException.class)
public void unreasonablePrediction(){
Consultant consultant = new Blogger();
Iterator iterator = mockIterator(consultant);
when(this.cut.company.iterator()).thenReturn(iterator);
this.cut.predictFutureOfJava();
}
public void unreasonablePrediction(){
Consultant consultant = new Blogger();
Iterator iterator = mockIterator(consultant);
when(this.cut.company.iterator()).thenReturn(iterator);
this.cut.predictFutureOfJava();
}
Listing 13: Returning the Mocked-Out Iterator
Mocking out the instance gives you control over which Consultant implementation is actually used. In our case, we can just use the Blogger implementation to check whether the IllegalStateException is properly thrown. You could also create a Consultant mock and return it instead of the real Blogger instance. This would be especially reasonable for a Consultant implementation with heavy dependencies on the Java EE platform.
The Event instance was also mocked out, which allows you to verify already-performed method invocations:
@Test
public void unreasonablePredictionFiresEvent(){
Consultant consultant = new Blogger();
Result expectedResultToFire = consultant.predictFutureOfJava();
Iterator iterator = mockIterator(consultant);
when(this.cut.company.iterator()).thenReturn(iterator);
try{
this.cut.predictFutureOfJava();
}catch(IllegalStateException e{}
verify(this.cut.eventListener,times(1)).fire(expectedResultToFire);
}
public void unreasonablePredictionFiresEvent(){
Consultant consultant = new Blogger();
Result expectedResultToFire = consultant.predictFutureOfJava();
Iterator iterator = mockIterator(consultant);
when(this.cut.company.iterator()).thenReturn(iterator);
try{
this.cut.predictFutureOfJava();
}catch(IllegalStateException e{}
verify(this.cut.eventListener,times(1)).fire(expectedResultToFire);
}
Listing 14: Testing the Event#fire Invocation
Testing of the unsuccessful case is a bit trickier. We swallow the IllegalStateException first and then check whether the method Event#fire was actually invoked.
It’s Like Java SE
Unit testing of Java EE 6 applications is no different than testing Java Platform, Standard Edition (Java SE). Java EE 6 components are just annotated classes. You should not treat them in a special way; instead, focus on the verification of the business logic.
It is a common mistake to test everything inside a container. Even a Java EE 6 container takes a few seconds to start and deploy your application. Testing your business logic in a container is just waste of time and introduces unnecessary complexity. In Java EE 6, you should always separate unit tests from integration tests. Unit tests and integration tests should be even separately executed. True unit tests with a mocked-out environment are lightening fast. The execution of PredictionAuditTest and OracleResourceTest don’t even take a half a second:
-------------------------------------------------------
T E S T S
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.abien.testing.oracle.boundary.OracleResourceTest
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 sec
Running com.abien.testing.oracle.control.PredictionAuditTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.025 sec
Results :
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
Listing 15: Test Results
Myth #1: You Need Interfaces for Mocking
The introduction of interfaces is often motivated by mocking. Modern frameworks, however, do not need interfaces for mocking any more. Mocks can be easily created directly from Plain Old Java Object (POJO) classes. PredictionArchiveResource uses the directly injected PredictionAudit class (see Listing 16).
@Path("predictions")
@Stateless
public class PredictionArchiveResource {
@EJB
PredictionAudit audit;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Prediction> allPredictions(@DefaultValue("5") @QueryParam("max") int max{
List<Prediction> allPredictions = audit.allPredictions();
if (allPredictions.size() <= max)
return allPredictions;
else
return allPredictions.subList(0, max);}
}
@Stateless
public class PredictionArchiveResource {
@EJB
PredictionAudit audit;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Prediction> allPredictions(@DefaultValue("5") @QueryParam("max") int max{
List<Prediction> allPredictions = audit.allPredictions();
if (allPredictions.size() <= max)
return allPredictions;
else
return allPredictions.subList(0, max);}
}
Listing 16: Injection of a POJO Class
Although PredictionAudit is a class and not an interface, it can be easily mocked out. To test the size limit of the returned list, the PredictionAudit class is completely mocked out (see Listing 17).
public class PredictionArchiveResourceTest {
PredictionArchiveResource cut;
@Before
public void initialize(){
this.cut = new PredictionArchiveResource();
this.cut.audit = mock(PredictionAudit.class);
}
@Test
public void allDecisionsWithMaxLesserReturn() throws Exception {
int expectedSize = 2;
List<Prediction> prediction = createDecisions(expectedSize);
when(this.cut.audit.allPredictions()).thenReturn(prediction);
List<Prediction> allDecisions = this.cut.allPredictions(3);
assertThat(allDecisions.size(), is(expectedSize));
}
public void initialize(){
this.cut = new PredictionArchiveResource();
this.cut.audit = mock(PredictionAudit.class);
}
@Test
public void allDecisionsWithMaxLesserReturn() throws Exception {
int expectedSize = 2;
List<Prediction> prediction = createDecisions(expectedSize);
when(this.cut.audit.allPredictions()).thenReturn(prediction);
List<Prediction> allDecisions = this.cut.allPredictions(3);
assertThat(allDecisions.size(), is(expectedSize));
}
@Test
public void allDecisionsWithMaxGreaterReturn() throws Exception {
int max = 5;
int expected = 3;
List<Prediction> prediction = createDecisions(max);
when(this.cut.audit.allPredictions()).thenReturn(prediction);
List<Prediction> allDecisions = this.cut.allPredictions(expected);
assertThat(allDecisions.size(), is(expected));
}
public void allDecisionsWithMaxGreaterReturn() throws Exception {
int max = 5;
int expected = 3;
List<Prediction> prediction = createDecisions(max);
when(this.cut.audit.allPredictions()).thenReturn(prediction);
List<Prediction> allDecisions = this.cut.allPredictions(expected);
assertThat(allDecisions.size(), is(expected));
}
@Test
public void allDecisionsWithMaxEqualReturn() throws Exception {
//obvious code omitted
}
public void allDecisionsWithMaxEqualReturn() throws Exception {
//obvious code omitted
}
List<Prediction> createDecisions(final int nr) {
return new ArrayList<Prediction>(){{
for (int i = 0; i < nr; i++)
add(new Prediction(Result.BRIGHT, true)); }};
}
}
return new ArrayList<Prediction>(){{
for (int i = 0; i < nr; i++)
add(new Prediction(Result.BRIGHT, true)); }};
}
}
Listing 17: Java Class Mocking
The only noteworthy business logic in the class PredictionArchiveResource is the limitation of the list size (see Listing 16). The maximum value of QueryParameter is used to compute the size of the sublist. A PredictionArchiveResource mock instance allows the return of a List<Prediction> with an arbitrary size. Controlling the list contents and size without accessing the JPA layer (or even the database) significantly simplifies the test. A mock also dramatically speeds up the test execution. A database access takes seconds, whereas accessing a mock is accomplished in milliseconds.
Java EE 6 made interfaces optional. You can inject classes (CDI managed beans and EJB beans) instead of interfaces without sacrificing any “enterprise” features. There is also no reason to use interfaces for mocking any more. Using plain classes, not interfaces, as a general building block not only makes the implementation of production code leaner, but it also simplifies testing. You do not have to decide whether to mock out a class or interface.
Conclusion
So far, we tested only the internal functionality of objects and methods. The surrounding infrastructure was completely mocked out to make the tests as simple as possible.
Our testing effort exactly fits the unit test definition: “In computer programming, unit testing is a method by which individual units of source code are tested to determine if they are fit for use. A unit is the smallest testable part of an application. In procedural programming a unit may be an individual function or procedure. In object-oriented programming a unit is usually a method…” (http://en.wikipedia.org/wiki/Unit_testing)
Our testing effort exactly fits the unit test definition: “In computer programming, unit testing is a method by which individual units of source code are tested to determine if they are fit for use. A unit is the smallest testable part of an application. In procedural programming a unit may be an individual function or procedure. In object-oriented programming a unit is usually a method…” (http://en.wikipedia.org/wiki/Unit_testing)
Although the results of all our unit tests are “green,” we don’t know whether the application is deployable. A JPA 2 mapping error, an inconsistent persistence.xml, or a misspelled JPA query just cannot and must not be tested with classic unit tests.
Infrastructure-dependent logic such as CDI event delivery, sophisticated dependency injection, or JPA queries can be tested only with integration tests in an environment similar to production. In a subsequent article, I will cover different flavors of integration tests.
Infrastructure-dependent logic such as CDI event delivery, sophisticated dependency injection, or JPA queries can be tested only with integration tests in an environment similar to production. In a subsequent article, I will cover different flavors of integration tests.
See Also
- Maven: http://maven.apache.org/
- Mockito: http://www.mockito.org/
No comments :
Post a Comment