2009/05/20 - Apache Shale has been retired.

For more information, please explore the Attic.

Shale Test Framework

Introduction

Modern application development processes have embraced the idea of unit testing as an integral step in creating high quality software. In the Java world, a popular framework for building and executing such unit tests is the JUnit framework. It is straightforward for an application developer to create a corresponding test case class for each class in the application itsef, and ensure that the tests contained in the test case get executed as part of the normal application build process.

One of the tenets of unit testing is that a test case should focus only on the methods of the class under test, in isolation from related application classes, or APIs provided by any container that the class under test might be installed into at runtime. But, how do you test an application class that has dependencies on such APIs (such as depending on the Servlet API to provide an HttpServletRequest object representing an incoming HTTP request)?

A popular answer to this dilemma is to utilize a library of mock objects -- classes that implement and emulate the container APIs, but still run in the isolated environment of a JUnit test case. Shale provides mock object implementations for its own features, as well as features of the underlying container (Servlet and JavaServer Faces) environment. In addition, convenient base classes are provided to make it very easy to build your own test cases utilizing these mock objects. This library is used to create unit tests for Shale components itself, but it is primarily focused on making it easy to build unit tests for application classes such as ViewControllers.

Provided Services

The Shale Test Framework provides mock object libraries, plus base classes for creating your own JUnit TestCases.

Mock objects are provided in package org.apache.shale.test.mock for the following container APIs:

  • JavaServer Faces
  • Servlet
These mock object classes implement the majority of the functionality defined by the container API Javadocs (although some methods currently throw UnsupportedOperationException). In addition, many of these classes support public methods, outside of the defined API, for configuring the object in a test environment. For example, MockServletContext includes addInitParameter() and setDocumentRoot() methods, to add context initialization parameters to the set returned via getInitParameter() and getInitParameterNames(), and to establish the base directory for resolving servlet context resources, respectively.

The org.apache.shale.test.base package contains abstract base classes that wire together instances of the various container API mock objects, in a manner similar to the way they would become available at runtime. The following base classes are available:

  • AbstractJsfTestCase - Base class for unit tests that require Servlet and JavaServer Faces objects to be available.
  • AbstractViewControllerTestCase - Extension of AbstractJsfTestCase that also provides convenient utility methods needed to test common scenarios in unit tests for ViewController implementation classes.
If you use one of these base classes, the setUp() method found there will initialize a set of protected instance variables for the container-managed objects you might need to access. The set of initialized variables includes (variable name and type):
  • application (MockApplication)
  • config (MockServletConfig)
  • externalContext (MockExternalContext)
  • facesContext (MockFacesContext)
  • lifecycle (MockLifecycle)
  • request (MockHttpServletRequest)
  • response (MockHttpServletResonse)
  • servletContext (MockServletContext)
  • session (MockHttpSession)

Using The Test Framework

The most common scenario for using the Test Framework is to construct test cases for ViewController implementation classes. Because the runtime environment of a ViewController is quite constrained, it is easy to construct isolated unit tests that exercise the methods exposed by a ViewController class. The Shale Use Cases web application (included in the distribution) contains many examples of such test cases, in the src/test directory. We will use org.apache.shale.usecases.locale.SelectTestCase (which tests the org.apache.shale.usecases.locale.Select implementation) as an example of how such a test case can be constructed.
  1. Create a new Java class SelectTestCase, in a package directory (typically under src/test in your project) that is the same as the package directory for the class you will be testing. This allows your test case to access package private and protected variables and methods in the class being tested.
  2. Make sure that the package declaration matches that of the class to be tested (in this case, org.apache.shale.usecases.locale.
  3. Declare your class to extend AbstractViewControllerTestCase (or, if you are not testing a ViewController implementation, extend AbstractJsfTestCase):
    public class SelectTestCase extends AbstractViewControllerTestCase {
      ...
    }
    
  4. Create a constructor that takes a String parameter, and passes it to the superclass constructor:
    public SelectTestCase(String name) {
        super(name);
    }
    
  5. Create a setUp() method and be sure to call super.setUp() at the beginning. This method will be called by JUnit immediately before it executes each test method.
    public void setUp() {
        super.setUp();
        // Customization will go here
    }
    
  6. After the call to the superclass setUp() method, perform any other initialization required to execute the tests in this test case. In our example case, a configuration method on the MockApplication instance will be used to define the default and supported Locales for this set of tests. This corresponds to what would happen at runtime, when the JavaServer Faces initialization process used the contents of the /WEB-INF/faces-config.xml resource to initialize these values. In addition, we will create a new instance of the Select class to be tested. It is important to create a new instance for each test, to ensure that execution of one test does not get influenced by the leftover property settings from a previous test.
    public void setUp() {
        super.setUp();
    
        // Configure the supported locales for this application
        List list = new ArrayList();
        list.add(new Locale("en"));
        list.add(new Locale("fr"));
        list.add(new Locale("de"));
        list.add(new Locale("es"));
        application.setSupportedLocales(list);
    
        // Construct a new ViewController instance
        vc = new Select();
    
    }
    
  7. Create a tearDown() method that cleans up any custom variables you allocated in your setUp() method, and then calls the super.tearDown() method. This will be called by JUnit after each test is executed.
    public void tearDown() {
        vc = null;
        super.tearDown();
    }
    
  8. Declare the custom instance variable(s) that you are setting up in your setUp() method. In this case, we create an instance of the ViewController class to be tested. A new instance will be created (via a call from JUnit to the setUp() method) before each test method is executed.
    // The instance to be tested
    Select vc = null;
    
  9. Create one or more individual test methods (which must be public, return void, take no arguments, and have a method name of the form testXXXX. For advice on how to construct such methods, consult the JUnit Web Site, or any of the large number of resources on the web describing how to use JUnit to build unit tests. The following example tests what happens when the select() method (which is executed when the Go button is pressed), but the value entered is not one of the valid options. NOTE that the test method must emulate the runtime calls to the ViewController event methods, because there is no actual runtime container available to perform these tasks automatically:
    // Test behavior of select() with an invalid value
    public void testSelectInvalid() {
    
        Locale locale = new Locale("en");
        facesContext.getViewRoot().setLocale(locale);
        vc.init();
        vc.preprocess();
        vc.setLocale("it");
        String result = vc.select();
        assertEquals(Select.FAILURE, result);
        checkMessageCount(1);
        assertEquals(locale, facesContext.getViewRoot().getLocale());
    
    }
    
    The test case sets the locale property (which is bound to a dropdown component at runtime, but we are simulating the behavior of Update Model Values here) to an invalid value, then calls the select() method. The test then verifies that the logical outcome returned matches that which is expected (Select.FAILURE), that there was an error message queued to be displayed, and that the locale for the current view was NOT actually changed.

  10. Finally, integrate the execution of this test case into your build script. Many IDEs will take care of this for you; however, if you are creating an Ant build script by hand, you might find the test target from the Shale Use Cases example a useful starting point. It locates all the test cases related to the entire application, and executes them:
      <target name="test" depends="test.compile"
       description="Execute unit tests">
    
        <mkdir          dir="${build.home}/test-results"/>
    
        <echo       message="Running unit tests ..."/>
        <junit printSummary="no" fork="yes"
              haltonfailure="yes" haltonerror="yes">
          <classpath  refid="test.classpath"/>
          <formatter   type="plain"
                    usefile="false"/>
          <formatter   type="xml"
                    usefile="true"/>
          <batchtest todir="${build.home}/test-results">
            <fileset    dir="${build.home}/test-classes"
                   includes="org/apache/shale/usecases/*/*TestCase.class"/>
          </batchtest>
        </junit>
    
      </target>