Things have improved, of course. Prior to EJB 3.0, there was virtually no other way of testing enterprise beans than actually deploying them in the container and testing them there.
The EJB 3.0 returned back to the roots by turning the enterprise beans into Plain old Java objects (POJOs). This revolutionary idea brings many advantages, one of them is making EJBs readily available to unit testing. You only have to mock the dependencies to the container.
The question arises though, how do you unit tests those parts that directly depend on the container, like the Data Access Objects (DAOs) which use container provided persistence services?
In this post, I will show on an example of a RESTful Web Service project with database persistence how it can be tested end-to-end without ever hitting the application server.
The Project
The example project contains the following RESTful Web Service and DAO to put under (unit) test:
@Path("/employees")
public class EmployeeResource {
@EJB
private EmployeeDao employeeDao;
@GET
@Path("/{param}")
@Produces(MediaType.APPLICATION_JSON)
public Employee get(@PathParam("param") long id) {
return employeeDao.find(id);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Employee> findAll() {
return employeeDao.findAll();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Long> add(Employee employee) {
Map<String, Long> response = new HashMap<>();
response.put("employeeId", employeeDao.create(employee));
return response;
}
}
@Stateless
public class EmployeeDao {
@PersistenceContext
private EntityManager em;
public Employee find(long id) {
return em.find(Employee.class, id);
}
public List<Employee> findAll() {
return em.createNamedQuery(Employee.FIND_ALL).getResultList();
}
public long create(Employee employee) {
em.persist(employee);
return employee.getEmployeeId();
}
}
Injecting DependenciesThe above classes are richly annotated. A naive attempt to test the classes with JUnit will succeed to compile but it will fail at runtime with a NullPointerException due to unresolved dependencies to EmployeeDao and EntityManager. So is there a way to resolve these dependencies outside of the application server?
Enter CDI — context and dependency injection. CDI was added in Java EE 6 as a general dependency injection mechanism. With CDI it is possible to annotate references to EJBs with @Inject instead of @EJB:
public class EmployeeResource {
@Inject
private EmployeeDao employeeDao;
...
This solves the DAO dependency nicely. However, the reference to the DAO will still be null because JUnit tests run with Java SE which does not support CDI. By now you can probably guess the central idea of the approach described in this post — to run the tests inside a CDI container instead of an application server. In the example project I use the JBoss Weld CDI container with Apache DeltaSpike which provides an abstraction layer to bootstrapping the container. You can inspect the project's Maven pom.xml file to see the actual library dependencies.How about the dependency to the EntityManager though? It is defined as a private field in the DAO class and it has to be set outside of the application server. Is there some way to do it, short of breaking the encapsulation?
Actually, the solution is simple if you think of the famous aphorism by David Wheeler:
All problems in computer science can be solved by another level of indirection.
The indirection is implemented, of course, with CDI. The DAO uses an @Inject annotation instead of @PersistenceContext:
public class EmployeeDao {
@Inject
private EntityManager em;
...
The definition of the entity manager in production/application server environment moves to a resource class which defines a CDI producer: public class Resources {
@PersistenceContext
@Produces
private EntityManager em;
}
In the test environment an alternative resource class is used: @Alternative
public class TestResources {
@Produces
@Singleton
private EntityManager createEntityManager() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hr_test");
return emf.createEntityManager();
}
}
There are two things to notice about the above class:- It is annotated with @Alternative. The alternative needs to be activated by specifying an <alternatives> tag in the beans.xml file. There are two beans.xml files in the example project — one in the production source code base (without <alternatives> tag so the persistence context is resolved using the Resources class) and another one in the test source code base.
- The above class creates an entity manager factory for the persistence unit hr_test. Alike beans.xml, there are also two persistence.xml files, for production and test respectively, in the example project.
The test classes also use CDI to resolve the references to the classes being tested:
@RunWith(CdiTestRunner.class)
public class EmployeeResourceTest {
@Inject
private EmployeeResource employeeResource;
@Test
public void testFindAll() {
List<Employee> employees = employeeResource.findAll();
assertThat(employees.size()).isGreaterThan(25);
}
}
Bootstrapping the ContainerSince the test classes are managed by CDI they need a custom JUnit test runner (specified with @RunWith) that has two responsibilities:
- Bootstrap the CDI container.
- Create instances of test classes using the CDI container.
public class CdiTestRunner extends BlockJUnit4ClassRunner {
static {
CdiContainer cdiContainer = CdiContainerLoader.getCdiContainer();
cdiContainer.boot();
}
public CdiTestRunner(Class<?> clazz) throws InitializationError {
super(clazz);
}
@Override
protected Object createTest() throws Exception {
return BeanProvider.getContextualReference(getTestClass().getJavaClass());
}
}
Adding Transaction SupportTransaction service is another service typically provided by the application server. In the standalone CDI test environment it is easy to add transaction support using an interceptor. The test methods are annotated with @Transactional and they can also specify whether the transaction should commit or rollback when the test finishes:
@Test
@Transactional
public void testCreate() {
final long employeeId = 401L;
Employee employee = new Employee(employeeId, "PKANE", "PR_REP", "Kane", new Date());
assertThat(employeeDao.create(employee)).isEqualTo(employeeId);
}
@Test
@Transactional(defaultRollback = false)
public void testCreateCommit() {
final long employeeId = 402L;
Employee employee = new Employee(employeeId, "JTOEWS", "PR_REP", "Toews", new Date());
assertThat(employeeDao.create(employee)).isEqualTo(employeeId);
}
@InterceptorBinding
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
boolean defaultRollback() default true;
}
@Interceptor
@Transactional
public class TransactionalRollbackInterceptor {
@Inject
private EntityManager em;
@AroundInvoke
public Object manageTransaction(InvocationContext ctx) throws Exception {
try {
em.getTransaction().begin();
return ctx.proceed();
} finally {
em.getTransaction().rollback();
}
}
}
@Interceptor
@Transactional(defaultRollback = false)
public class TransactionalCommitInterceptor {
@Inject
private EntityManager em;
@AroundInvoke
public Object manageTransaction(InvocationContext ctx) throws Exception {
em.getTransaction().begin();
Object result = ctx.proceed();
em.getTransaction().commit();
return result;
}
}
ConclusionThat's it. If you want to check the entire example project's source code, you can find it in Subversion or download it as a zip archive.
Bonus Picture
As I was taking a break while writing the example project, our cat leapt on the table and took a sharp look on the source code, as if she was reviewing it. I was lucky enough to have my camera at my fingertips and I captured the moment:
- Bean-Testing von Java EE-Anwendungen mit CDI — this article (in German) describes a somewhat different approach than I have taken here: instead of using the Resource pattern, the authors resolve the dependencies by scanning the classes at runtime and adding the @Inject annotations programmatically.
- Java EE 7 Tutorial: Building RESTful Web Services with JAX-RS
- RESTEasy Hello World Example
- Jboss / Wildfly maven plugin to deploy on localhost/remote server
Cool pictured. Thanks for the post. The new JEE is much better than J2E in my opinion. The POJOS make a big difference especially with unit testing. Of course, we all have to write our unit tests ;)
ReplyDeleteAbsolutely.. the point here is to make writing and running unit tests as delightful as possible. This means avoiding boilerplate code in the tests and having them run really fast. The CDI test approach addresses both these aspects.
Deletedo you have the sql to create the employees table? It may be better to use an in memory db in this example instead.
ReplyDeleteI like your example a lot! I think we are going to being testing in the fashion here! Currently, we make all the injected attributes with the default access modifiers and set the attribute using Mockito. It is truely unit but really skips all the annotations which are a big piece of the puzzle and should be tested with the rest of the class. We also have integration tests that launch Cargo but that seems a bit expensive to test each class. Here is a snippet of the sql to create the table. I switched to use mySql instead of Oracle:
ReplyDeleteCREATE TABLE employees (
COMMISSION_PCT DOUBLE,
DEPARTMENT_ID BIGINT,
email VARCHAR(20),
EMPLOYEE_ID BIGINT,
FIRST_NAME VARCHAR(20),
LAST_NAME VARCHAR(25),
PHONE_NUMBER VARCHAR(25),
HIRE_DATE DATE,
JOB_ID VARCHAR(20),
death DATE,
salary DOUBLE,
MANAGER_ID BIGINT,
PRIMARY KEY(EMPLOYEE_ID)
);
insert into employees (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, email) values (100, 'Joe', 'Smith', 'SKING');
in your test you will need 25 more records for your unit test to pass (or change the assert in the unit test)
Francisco, thanks for your comments and for posting the SQL! This is as you figured out from the Oracle HR schema.
DeleteI fully agree with you that it would be better to use an in-memory database. In fact I have to admit that the test of the getAll method is flawed since it depends on pre-existing data in the database.
The best practice for integration tests dealing with the database is to force the database into a known state before each test. Tools exist to create the known state, most notably DbUnit and DbSetup.
i am attempting to run your example with j2ee 6.0 but i keep on getting the following because the EntityMgr is failing to be injected by Weld. Could you provide any insight?
ReplyDeleteWELD-000044 Unable to obtain instance from null
I think the reason you get the error is that the project depends on default datasource (there is no jta-data-source definition in persistence.xml) which is a Java EE 7 feature (for more see https://blogs.oracle.com/arungupta/entry/default_datasource_in_java_ee).
DeletePlease add a datasource to persistence.xml and try again.