Testing Java EE (or Why Integration Tests Are Overrated)
Testing that goes beyond just unit testing and integration testing. Why we should be doing full system testing.
Join the DZone community and get the full member experience.
Join For FreeIn (enterprise) development testing often is treated very much as poor relations. But for more than “Hello World” applications testing is not just nice to have (at the end of the project) rather than crucial for working software. How to test Java EE applications in a productive yet comprehensive way?
A good test has following criteria: It must be fast, reliable and easy to write and maintain.
There are several ranges of testing like unit, integration or system tests. The smaller the scope of the test is the easier it is to write and to maintain, the faster it runs but also the more “far away” it is from the real world application.
IMO following test scopes should be applied in certain situations.
Unit Tests
Unit tests cover the business logic of a single component — usually a class — and provide very fast feedback for the developers without needing to startup any container.
The simplest way to unit test Java EE components is to use plain JUnit without any special runner and Mockito to mock away every other involved component.
@Stateless
public class TaskStore {
@PersistenceContext
EntityManager entityManager;
public List<Task> listAll() {
return entityManager.createNamedQuery("Task.findAll", Task.class).getResultList();
}
public List<Task> filterAll(final Filter filter) {
return listAll().parallelStream().filter(filter::matches).collect(Collectors.toList());
}
...
}
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
public class TaskStoreTest {
private TaskStore cut;
@Before
public void setUp() {
cut = new TaskStore();
cut.entityManager = mock(EntityManager.class);
TypedQuery mockQuery = mock(TypedQuery.class);
when(cut.entityManager.createNamedQuery(anyString(), any())).thenReturn(mockQuery);
when(mockQuery.getResultList()).thenReturn(sampleTasks());
}
@Test
public void testFilterContextMatch() {
Filter filter = new Filter();
...
List<Task> filteredTasks = cut.filterAll(filter);
assertEquals(1, filteredTasks.size()); assertEquals(...);
verify(cut.entityManager).createNamedQuery("Task.findAll", Task.class);
}
}
As EJBs and CDI managed beans are basically POJOs with annotations, it’s trivial to create or mock them in a test.
@Inject
ed dependencies can be mocked and manually modified in the class due to the fact that the test resides in the same package as the component and can access the package-private defined property. Not all developers may agree to making the properties not private
just for testability. However, this enables the test to easily modify the dependencies without the need for (slow) Reflection. And the Java EE implementation will prevent production code to reset the dependencies of any managed bean.
With that approach one can run hundreds of tests in no time. Any special @RunWith
runner definition should be considered over-engineering for a simple unit test — with the exception of Parameterized
tests. The latter are in fact very helpful for mass tests.
In general: You can be creative in how to write tests, but they must run fast and simplicity wins.
Integration Tests
Integration tests in a Java EE environment usually mean to combine a few components in a sort of emulated container. Test frameworks like Arquillian can be used to accomplish this.
The problem with this approach is that it consumes much more time than a unit test as the container has to be started every time. That is why it can be a problem to cover business logic with integration tests.
The killer use case however is to test CDI extensions and technical Java EE “plumbing” — rather than business logic. Testing custom CDI producers or custom scopes for example is a perfect scenario. But: These tests should be the big exception in your application.
System Tests
Unit and integration tests are nice to get a fast feedback but if your application is solely tested by them you’ll get other errors on production (like environment-specific configuration, problems with the server environments or external interfaces, etc.). A full system test on the other hand covers all business use cases end-to-end. The application should run in the same way during the test as it would in production. That means the system test environment is ideally the same — including server configuration, databases, network, etc. — with the only difference that it’s not the real production server. During the tests all external systems are mocked away and ensure that the application communicates with them in a correct way.
Having that said means also that integration tests which combine a few components are somewhat overrated. Your application can’t rely on integration tests only and then the feedback time saved is in general not worth the overhead of writing and maintaining them.
That is why I recommend to only have simple unit tests for fast feedback and fully-fledged system tests — with the exception that integration tests sometimes help to test Java EE “plumbing”.
An easy way to achieve an identical system for all environments is to use containers. The container which incorporates the application, the application server and the operating system is built once from the CI server and used for all environments. Using this approach highly increases the stability as exactly the same artefacts run in production which have been tested extensively.
Conclusion
From my point of view a productive and yet comprehensive Java EE testing pipeline looks like follows:
- Unit tests with plain JUnit (no
@RunWith
s needed) - Integration tests for business logic considered harmful
- Full system tests which cover all business use cases
- Continuous Deployment pipeline deploying and starting the system tests automatically
- Automated performance / stress tests depending on the requirements
- Deploying the tested application (ideally as container) in production
Published at DZone with permission of Sebastian Daschner. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments