Project Description

WebFamulus is a web application using the Quartz scheduler framework for process automation. It provides a web interface to schedule, monitor, and interact with Java processes that are configured as 'jobs'. Access to user and administrative interfaces is based on permissions and group ownership. Likewise, the configuration of jobs, alerts, performance monitoring and statistics is based on group ownership and permissions granted to an individual user. The application is currently under development and a beta-version is available to selected clients.


Typical applications for the Quartz framework are network and hardware monitors, database maintenance, reporting, etc. Tedious, time-consuming tasks can run repeatedly whenever possible following sophisticated schedules, including selected dates and weekdays, black-out times, holidays, etc. The field of application, however, is limited only by what the programming language cannot provide. Quartz makes it possible to schedule and execute automatically just about anything that can be programmed in Java.


WebFamulus, in addition, supports jobs to be executed as sequences based on the (optional) evaluation of the preceding job's exit status. Furthermore, the application is backed by an extensive database that makes it possible to let non-administrative users create, configure, interact with, and monitor their own jobs, alerts, and reports on-line or via electronic mail.


2014-02-09

Spring Batch Testing with TestNG

The following blog is a brief description of how to test a simple batch application purely with TestNG, without having recourse to the Spring TestContext Framework. The application happened to use Spring Batch, but the testing techniques are of course also applicable to an application written for JSR 352 (Batch Processing for Java Platform).



1. BatchArchiver: The Spring Batch Application

The BatchArchiver is a simple Spring Batch job developed to archive database back-up files. The database is instructed to dump a complete back-up every day at various times into a dedicated directory, using a formatted timestamp as a file name prefix. Once a month, files older than N days are moved to a different location, whereby only the last file of any given day is kept and the others are discarded. The remaining files are collected in a ZIP archive and moved to storage.

The application uses as external Java libraries Joda-Time (Joda.org) for date and time calculations and zt-zip (ZeroTurnaround) for the generation of ZIP archives. It was developed using Eclipse as an IDE and Maven as a build tool.

The batch process runs as follows:

  1. The application tries read the source directory where the back-up files are located. If it is not accessible, the job throws an Exception an exits.
  2. A temporary working directory and, if it does not exist already, a storage directory for the final JAR file are created.
  3. The application reads the contents of the source directory. It creates an array of file objects whose creation date (last modified) is older than (predates) the configured cut-off date.
  4. All files older than the cutoff-date are copied into the temporary working directory. The corresponding in the source directory files are then deleted.
  5. The name of every file contains starts with a formatted timestamp of its date and time of its creation. This design ensures that sorting the files names will result in an ordered sequence reflecting the creation date. Based on this information, the application deletes all files except the last one created on every given day. At the end of this step, the working directory contains one file for every distinct day in the data set.
  6. The remaining files in the working directory are collected in a JAR file which, after the compilation is finished, is moved to the storage directory.
  7. Finally, a clean-up step deletes the temporary working directory.

Test Utilities

From a programmatic point of view, the process of the job is not complicated. It uses very basic operations: accessing, creating, and deleting files (where directories and actual files are both Java File objects). Every step of the batch job presupposes a directory structure in order to locate and/or deposit files. And, of course, the tests can only run if there are files to work with. For this purpose, a small utility program was written that can create, read, and delete directories, as well as create, write, and delete files. Since the batch job filters files based on date-time criteria, the utility can also generate series of timestamps (for a specified date range with variable number of instances per day) that are used for file name patterns and modification dates. It is thus possible to quickly create even large datasets of test files quickly and share the data sets among different test cases where appropriate.

Test Phases

It is understood that before the tests described here are undertaken, the class library of the batch job have been unit-tested. Unit tests are run during the test phase of the Maven build process and focus on individual classes methods, that is, small-grained details of the code. Integration tests, focussing on complete steps and transitions from one step to another, are run later during the integration-test phase. This separation is of great use when the code base is large and tests need a long time to complete, as both phases can be separated. It is irrelevant, however, for this small application where running the entire test suite is just a matter just seconds.

TestNG is used as a framework in both test phases. The possibilities to run parameterized tests (providing runtime parameters through configuration) and data-driven tests with the help of data providers (data generators that call test methods with different sets of data) are a great convenience when the test data is applicable to more than one method, class, or programming unit. More importantly, however, it makes it easy to run tests with any number of configurations and test data variables. Additional test cases are quickly implemented, if need be, by adding to the test data.

2. Unit Tests

Since Spring Batch processing steps are a single or combination of plain Java classes, all aspects of a batch job can be easily isolated and tested. The focus here is not on the product of steps, tasklets, or chunks, but rather on class methods.

Testing Tasklets

Tasklets commonly consist of reader, processor, and writer classes, many of which are provided by Spring Batch out-of-the-box. These do not need to be unit-tested. Custom implementations can be tested as POJOs and need to call the open(), process(), and write() methods, respectively, as well as any necessary helper methods. Since various steps in the batch process share similar operations (reading and writing files, directories, etc.), the unit tests can share the test data as well. TestNG's data provider facilitates testing by making the test data accessible without having to recreate the data for each test. Furthermore, if the set of test data increases, it is automatically delivered to all tests without any changes to the test code.

3. Integration Tests

Here TestNG's parameterized testing capabilities prove to be a great tool for running a large number of tests with minimal configuration. The sequence of steps of a Spring Batch job is configured in XML files. This configuration file determines which Java classes are used to execute a step and which step is the next one to execute. Batch jobs and steps are thus collections of Java classes that are executed in sequence. The execution of each individual class has been tested during the unit test phase. Integration testing here takes a job step as the basic unit and asserts the correctness of the product of each step.

Proceeding in four stages, the basic structure of an integration test is as follows:

  1. Set-up: prepare the test data.
  2. Run the job.
  3. Assert the correctness of the last step's output.
  4. Clean-up.

Since a batch job is a sequence of executions defined by a configuration, the sequence will change when the configuration is changed. Hence, we'll perform step-by-step tests by creating configuration files for each step test:

  • test-class 1: step 1
  • test-class 2: steps 1 and 2
  • test-class 3: steps 1, 2, and 3
  • etc.

The actual tests occur in the third stage and must be implemented in separated class files. The remaining stages--set-up, job execution, and clean-up--are the same for all tests and can be implemented in a superclass that each test class inherits.


public BaseTest
{
     @BeforeTest
     @Parameters( { "jobContextFile", "applicationContextFile" } )
     public void configure( @Optional("none") String jobContextFile, 
                            @Optional("none") String applicationContextFile )
     {
         // read the configuration files passed in as runtime parameters
     }
 
 
     @BeforeTest
     @Parameters( { "createDirectoryList" } )
     public void createTestDirectories( @Optional("none") String createDirectoryList ) throws IOException
     {
         // create the test directory structure for the job to run
     }
 
     @BeforeTest
     @Parameters( { "targetDateOffsetInDays", "daysBefore","daysAfter", "fileInstancesPerDay" } )
     public void initialize( @Optional("-1") int targetDateOffsetInDays, 
        @Optional("-1") int daysBefore, 
        @Optional("-1") int daysAfter, 
        @Optional("-1") int fileInstancesPerDay )
     {
         // create the test data and run the job,
         // based on runtime parameters
     }
 
     @AfterTest
     public void cleanup() throws Exception
     {
         // delete test data 
     }
}

Through TestNG's @Parameter annotation it is now also possible to run each test class with different configuration files and test parameters:

<suite name="BatchArchiver.noProcess.test.driver" verbose="3">

    <test name="steps.integration.test.step05.1" preserve-order="true">
        <parameter name="jobContextFile" value="jobContext/in-memory-context-step05.xml" />
        <parameter name="applicationContextFile" value="appContext/in-memory-application-context.xml" />
        <parameter name="daysBefore" value="25"/>
        <parameter name="daysAfter" value="3"/>
        <classes>
            <class name="com.wayaleshi.batch.job.InMemoryJobStep05Test" preserve-order="true"/>
        </classes>
    </test>
 
    <test name="steps.integration.test.step05.2" preserve-order="true">
        <parameter name="jobContextFile" value="jobContext/in-memory-context-step05.xml" />
        <parameter name="applicationContextFile" value="appContext/in-memory-application-context.xml" />
        <parameter name="daysBefore" value="43"/>
        <parameter name="daysAfter" value="1"/>
        <classes>
            <class name="com.wayaleshi.batch.job.InMemoryJobStep05Test" preserve-order="true"/>
        </classes>
    </test>
        
</suite>