Wednesday, June 30, 2010

Testing JSF backing beans

No matter what level of unit and integration testing is performed on one's code, there still seems to be no substitute for running tests directly from the user interface.  And while I've tried my hand at automating the UI tests for web applications, I find the task quite tedious and the scripts/code difficult to maintain.  So I find myself having to manually perform a battery of UI tests on my web application, enduring the dreaded Tomcat deploy/restart cycle (no snickering from the Ruby on Rails audience please!).

In particular, I find that the use of Hibernate and Spring's declarative transaction management makes it difficult to guarantee within my integration tests that entity objects are always being properly managed by the current Hibernate session.  That is, ensuring that LazyInitializationExceptions, NonUniqueObjectExceptions, etc., will not be thrown for a given call stack of UI backing bean and service methods.  This is difficult to test for "outside of the container", in a standalone integration test.  The complexity stems from the fact that backing bean methods (the entry points of UI actions) and the service layer methods that they call can each be transactional.  So the developer must guarantee that the entities being passed into a transactional method comply with the method's expectations for the "persistent", "detached", or "transient" state of these entities.  Things get really ugly when the domain model's save/update/persist cascades are different than what are needed to reattach all of the related entities that are needed by the method being called.

In my web application, I use Spring to instantiate all of the JSF backing bean objects, making use of the Spring-provided DelegatingVariableResolver.  (This allows the backing beans to be proxied and endowed with Spring's AOP functionality, for declarative transactions, logging aspects, etc.)  But testing backing beans with this design is made difficult by the fact that they must be instantiated by Spring within the test. This is solved by using AbstractDependencyInjectionSpringContextTests, which allows us to create a Spring context from which we can retrieve our backing beans.  However, this is still not enough, since in my case, the backing beans use "session" scope, and so normally require that they are instantiated within a servlet container, or at least that a FacesContext can be acquired.

To avoid the servlet container/FacesContext requirement, I figured out that I could create a MockSessionScope that can emulate a single extant servlet session, without JSF being initialized. This allows us to instantiate and retrieve our JSF backing bean objects from Spring, within our integration tests, and make calls on the backing bean methods with all AOP behavior enabled.  In this way, we can recreate the full call stack into our application, as if our JSF framework was calling the backing bean directly in response to an HTTP request.  And without using a browser client (real or headless).  Most significantly, we are now able to test the full transactional context that exists when our backing bean and service methods are called, allowing us to detect and debug problems merely by running our integration tests.  No more Tomcat deploy/restart/manually-testing cycle!

To make use of this we simply need to define a CustomScopeConfigurer in our testing spring context configuration that specifies the MockSessionScope for the "session" scope:

  <property name="scopes">
      <entry key="session">
        <bean class=""/>

The one thing that still is not exercised by this testing design is the rendering of the web pages, which can still be a source of problems.  Note that it is also necessary to ensure that the UI layer code being tested does depend upon a FacesContext or an HttpSession.  For the former, see various approaches suggested by others. For the latter, one can use Spring's MockHttpSession, as necessary.  The above design thus has some drawbacks, but we have at least raised the level of our tests one step closer to automated UI testing, and without the pain.