17.6. The SimpleSensor: Unit testing

The first part of this chapter showed how you can implement a simple sensor using nothing more than the sensorshell.jar file as a library. The second part of this chapter showed how to create a Hackystat module around that implementation. This section shows how to enhance the hackyDoc_SimpleSensor module to perform a simple unit test of the sensor.

Conceptually, the unit test of a sensor involves two basic parts. First, you want to invoke the sensor and have it send data off to a Hackystat server. Second, you want to login to that Hackystat server and check to see whether the data is there. Fortunately, the Hackystat Framework provides a number of facilities to support this kind of testing. They include:

Creating a unit test for the Simple Sensor required changes to the local.build.xml file and the SimpleSensor.java file. Finally, it required the creation of a new file containing the test itself, which is called TestSimpleSensor.java. The next sections discuss the changes made to the first two files and the new test file.

17.6.1. The local.build.xml file

Example 17.16, “The local.build.xml file” shows the enhanced version of the hackyDoc_SimpleSensor build file. The changed parts of the file are in bold. Since the last two targets did not change, their bodies are omitted.

Example 17.16. The local.build.xml file

 
<project name="hackyDoc_SimpleSensor.local" default="hackyDoc_SimpleSensor.default">
  <description> 
  Provides documentation code and packaging for the Sensor Design chapter in the developer guide.
  </description>
   
  <dirname property="hackyDoc_SimpleSensor.local.basedir" file="${ant.file.hackyDoc_SimpleSensor.local}"/>
  <property name="hackyDoc_SimpleSensor.src.dir" location="${hackyDoc_SimpleSensor.local.basedir}/src"/>
  <property name="hackyDoc_SimpleSensor.required.modules" value="hackyCore_Kernel, hackySdt_DevEvent, hackyCore_Common"/>

  <path id="hackyDoc_SimpleSensor.classpath">
    <pathelement location="${install.war.web-inf.classes.dir}" />
    <fileset dir="${install.war.web-inf.lib.dir}">
      <include name="*.jar"/>
    </fileset>
  </path>
   
  <target name="hackyDoc_SimpleSensor.compile" if="hackyDoc_SimpleSensor.available"
    description="Compiles changed classes to WEB-INF/classes directory.">
    <hackyCore_Build.javac srcdir="${hackyDoc_SimpleSensor.src.dir}" source="1.5">
       <classpath refid="hackyDoc_SimpleSensor.classpath"/>           
    </hackyCore_Build.javac>
  </target>
 
  <target name="hackyDoc_SimpleSensor.install.post-sensorshell" if="hackyDoc_SimpleSensor.available" 
    depends="hackyDoc_SimpleSensor.createJarFile"/>
  
  <target name="hackyDoc_SimpleSensor.junit" if="hackyDoc_SimpleSensor.available"   description="Runs JUnit.">
    <makeJUnit module.name="hackyDoc_SimpleSensor" module.package.prefix="org.hackystat.doc.simplesensor"/>
    <makeEcho message="Completed hackyDoc_SimpleSensor.junit." prefix="hackyDoc_SimpleSensor.junit"/>
  </target>  
  
  <target name="hackyDoc_SimpleSensor.checkJarsUptodate" 
   (body omitted)
  </target>

  <target name="hackyDoc_SimpleSensor.createJarFile" 
   (body omitted)
  </target>    
  
  <target name="hackyDoc_SimpleSensor.default" description="Does nothing. Required by Ant."/>
</project>

Let's discuss each of modifications in turn. The first one is a change to the "required.modules" property:

<property name="hackyDoc_SimpleSensor.required.modules" value="hackyCore_Kernel, hackySdt_DevEvent, hackyCore_Common"/>

The change is the addition of the hackyCore_Common module as a required module for the hackyDoc_SimpleSensor module. This module is now required because the TestSimpleSensor class imports test framework classes from the hackyCore_Common module. These classes enable the SimpleSensor unit test to easily check to see if the DevEvent sensor data exists on the server. We will discuss this further below.

The second modification is to the ".classpath" property:

  <path id="hackyDoc_SimpleSensor.classpath">
    <pathelement location="${install.war.web-inf.classes.dir}" />
    <fileset dir="${install.war.web-inf.lib.dir}">
      <include name="*.jar"/>
    </fileset>
  </path>

The change results in the adding of all of the jar files in the ${install.war.web-inf.lib.dir} directory to the classpath. This has the effect of ensuring that the JUnit library file is on the classpath, which is required in order to compile the TestSimpleSensor class. Rather than add just that single file to the classpath, we add all of the jar files in the WEB-INF/lib directory. This is overkill, but it eliminates the need to hardcode the name of the JUnit jar file. You can choose to add just the JUnit jar file if you prefer in your own code.

The third modification is the creation of a new target called "hackyDoc_SimpleSensor.junit":

  <target name="hackyDoc_SimpleSensor.junit" if="hackyDoc_SimpleSensor.available"   description="Runs JUnit.">
    <makeJUnit module.name="hackyDoc_SimpleSensor" module.package.prefix="org.hackystat.doc.simplesensor"/>
    <makeEcho message="Completed hackyDoc_SimpleSensor.junit." prefix="hackyDoc_SimpleSensor.junit"/>
  </target>  

This target must be named according to the template [modulename].junit, and use the "if" clause to ensure it is not run unless the module is specified as "available". Inside, it uses the "makeJUnit" macro to run all of the unit tests (i.e. the Java classes whose name begins with "Test") in the module. The "makeEcho" macro prints out an informative message to the command shell when this target completes execution.

It is vitally important to run AutoConfig after these changes to the local.build.xml file. This is because the AutoConfig command implicitly defined a default (empty) implementation of the hackyDoc_SimpleSensor.junit target when that target did not exist in the local.build.xml file. By re-running AutoConfig, the Hackystat build system will now recognize that the junit Ant target for this module is now available in the local.build.xml file:

C:\svn\hackyCore_Build>ant -f autoconfig.build.xml
Buildfile: autoconfig.build.xml

run:
    [mkdir] Created dir: C:\svn\hackyCore_Build\build\autoconfig
     [echo] [AutoConfig] Generated modules.build.xml and sample.hackystat.build.properties.

autoconfig.build.default:

BUILD SUCCESSFUL
Total time: 1 second
C:\svn\hackyCore_Build>ant

Given these changes to the local.build.xml file (as well as the changes to SimpleSensor.java and TestSimpleSensor.java), we can run the Hackystat build system to build and test our module as follows. There is only one change in the output compared to the invocation in previous sections. This additional line is highlighted in bold:

C:\svn\hackyCore_Build>ant -q freshStart all.junit
     [echo] (15:34:55) Completed hackyCore_Build.checkModuleAvailability
     [echo] (15:34:56) Completed hackyCore_Build.hotUndeployHackystat
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
     [echo] (15:35:19) Completed all.compile
     [echo] (15:35:45) Completed all.checkstyle
     [echo] (15:35:52) Completed all.install.pre-sensorshell
     [echo] (15:36:03) Completed hackyCore_Build.unjarSensorShellFiles
     [echo] (15:36:06) Completed hackyCore_Build.installSensorShell
     [echo] (15:36:38) Completed all.install.post-sensorshell
     [echo] (15:36:38) Completed hackyCore_Build.deployTestData
     [echo] (15:36:46) Completed hackyCore_Build.hotDeployHackystat
     [echo] (15:37:01) Completed hackyCore_Kernel.junit.
     [echo] (15:37:07) Completed hackyCore_Installer.junit.
     [echo] (15:37:08) Completed hackyCore_Statistics.junit.
     [echo] (15:37:20) Completed hackyCore_Report.junit.
     [echo] (15:37:31) Completed hackyCore_Common.junit.
     [echo] (15:37:33) Completed hackyCore_Telemetry.junit.
     [echo] (15:37:46) Completed hackySdt_Activity.junit.
     [echo] (15:37:49) Completed hackySdt_FileMetric.junit.
     [echo] (15:37:52) Completed hackySdt_WorkspaceMap.junit.
     [echo] (15:37:55) Completed hackySdt_DevEvent.junit.
     [echo] (15:37:58) Completed hackyDoc_SimpleSensor.junit.
     [echo] (15:37:58) Completed all.junitReport
     [echo] (15:37:58) Completed all.junit

BUILD SUCCESSFUL
Total time: 3 minutes 6 seconds
Sending build result to Hackystat server... Done!
C:\svn\hackyCore_Build>

This output indicates that our changes to the module resulted in the TestSimpleSensor file being compiled successfully, and that our JUnit test case ran without errors.

17.6.2. The SimpleSensor.java file

In order to support unit testing of the Simple Sensor, we need to refactor the code so that the SimpleSensor can be instantiated in "test mode". This "test mode" involves the ability to override the default values for the hackystat host and user key found in the developer's sensor.properties file with a "test" host and "test" user.

To make this concrete, consider a developer who has installed hackystat sensors and is sending data on her software development projects to the public Hackystat server at http://hackystat.ics.hawaii.edu/. Let's say her user key for this server is "XYZZY". These values are stored in her sensor.properties file in her .hackystat directory.

Let's assume that one of the systems that this developer is enhancing is Hackystat, and so she has downloaded the sources and built a local version of Hackystat that she is running tests on. By default, this local development version of the Hackystat server is located at http://localhost:8080/. Furthermore, this local development server won't have any knowledge of the developer's user key for the public Hackystat server. How does testing proceed in this situation?

The answer is that there are two constructors for the SensorProperties class. The default no-arg constructor illustrated in the original sensor code will read values for the Hackystat host and user key from the local user's sensor.properties file, which is what we want under normal circumstances. However, there is a second constructor that takes two strings as arguments corresponding to the Hackystat host and user key. This second constructor allows developers to build a special "testing" SensorProperties instance with different host and user key values than those stored in the sensor.properties file. So, for example, our developer can write test cases that use "http://localhost:8080/" as the Hackystat host and "testsimplesensor" as the user key.

Example 17.17, “The SimpleSensor.java file” shows the refactored version of the SimpleSensor.java file that allows an alternative instantiation of the sensor for testing purposes.

Example 17.17. The SimpleSensor.java file

 
package org.hackystat.doc.simplesensor;
import java.util.Date;

import org.hackystat.core.kernel.admin.SensorProperties;
import org.hackystat.core.kernel.shell.SensorShell;

/**
 * Provides the SimpleSensor sensor implementation.
 * @author Philip M. Johnson
  */
public class SimpleSensor {
  
  /** Holds the SensorShell instance, initialized to the 'real' or 'test' environment. */
  private SensorShell shell; 

  
  /**
   * The 'normal' SimpleSensor constructor, which initializes the SensorShell
   * to the user and host in the local sensor.properties file. 
   */
  public SimpleSensor() {
    SensorProperties sensorProperties = new SensorProperties("SimpleSensor");
    this.shell = new SensorShell(sensorProperties, false, "SimpleSensor");
  }
  
  /**
   * The 'test' SimpleSensor constructor, which initializes the SensorShell
   * to the passed "test" host and "test" userKey. 
   * @param host The host to which the SimpleSensor will send data. 
   * @param userKey The userKey to be associated with the data sent by this sensor. 
   */
  public SimpleSensor(String host, String userKey) {
    SensorProperties sensorProps = new SensorProperties(host, userKey);
    this.shell = new SensorShell(sensorProps, false, "test", false);
  }
  
  /**
   * Adds the specified DevEvent to this SimpleSensor.
   * @param tstamp The timestamp for this DevEvent.
   * @param type The type field for this DevEvent.
   * @param path The path field for this DevEvent.
   */
  public void addDevEvent(Date tstamp, String type, String path) {
    this.shell.doCommand(tstamp, "DevEvent", "add", "tool=SimpleSensor", 
        "type=" + type, "path=" + path);
  }

  /**
   * Sends any remaining DevEvents to the hackystat server. 
   * @return True if the sending was successful. 
   */
  public boolean send() {
    return this.shell.send();
  }
  
  /**
   * The "Hello World" sensor.  Sends a DevEvent when invoked from command line.
   * Accepts one argument, the DevEvent "type" string, which defaults to "Invoked"
   * Uses the current directory as the DevEvent "path" value. 
   * @param args A single string providing the DevEvent "type".
   */
  public static void main(String[] args) {
    String type = (args.length > 0) ? args[0] : "Invoked";
    SimpleSensor sensor = new SimpleSensor();
    sensor.addDevEvent(new Date(), type, System.getProperty("user.dir"));
    sensor.send();
  }
}

In this new implementation to support testing, we no longer put all of the code in the main() method, which is good because that was bogus anyway. Instead, the SimpleSensor class now provides a realistic, object-oriented API consisting of two constructors, two methods (addDevEvent and send), and a private instance variable (shell).

The default constructor returns an instance of the SimpleSensor where its internal SensorShell has been instantiated using the values in the sensor.properties file. The SimpleSensor also provides an alternative constructor for testing that takes a host and user key and uses those values to instantiate the SensorShell. Note that in this testing-oriented instantiation, offline storage in the SensorShell is disabled.

The internal SensorShell instance is not available to clients. Instead, clients manipulate the SimpleSensor using two methods: addDevEvent (to add a new DevEvent sensor data instance) and send (to send off any remaining cached sensor data to the server).

The main() method shows an example use of this API. The main method instantiates the SimpleSensor using the default constructor. Next, the addDevEvent method is invoked on the SimpleSensor instance to add a DevEvent. The last line of the main method ensures that the data to the server. The next section shows how we use this API in a test case.

17.6.3. The TestSimpleSensor.java file

Now that we've seen the changes to the local.build.xml file, which support compilation and testing using Ant, and changes to the SimpleSensor.java file in order to support instantiation and use of the SimpleSensor with a test host and user key, let's now look at the TestSimpleSensor class, which contains our test case. Example 17.18, “The TestSimpleSensor.java file” shows the code.

Example 17.18. The TestSimpleSensor.java file

 
package org.hackystat.doc.simplesensor;

import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Date;
import org.hackystat.core.common.selector.day.StartDayTestSelector;
import org.hackystat.core.common.selector.sdt.SensorDataTypeTestSelector;
import org.hackystat.core.kernel.admin.ServerProperties;
import org.hackystat.core.kernel.test.HackystatTestConversation;
import org.hackystat.core.kernel.test.HackystatTestParameters;
import org.hackystat.core.kernel.util.DateInfo;

/**
 * Tests the SimpleSensor by sending data to the test server.
 * @author Philip Johnson
 */
public class TestSimpleSensor {
  
  /** 
   * Tests the "happy path": sending and retrieval of a DevEvent sent by the SimpleSensor. 
   * @throws Exception If errors occur while accessing server-side data. 
   */
  @Test 
  public void testSimpleSensorHappyPath() throws Exception {
    // Set up the sensor with the test host and test user. 
    ServerProperties serverProperties = ServerProperties.getInstance();    
    String testUser = "testsimplesensor";
    String host = serverProperties.getHackystatHost();
    SimpleSensor sensor = new SimpleSensor(host, testUser + serverProperties.getTestDomain());

    // Send a sample DevEvent to the test host and test user. 
    Date testDate = new Date();
    sensor.addDevEvent(testDate, testDate.toString(), System.getProperty("user.dir"));   
    boolean OK = sensor.send();

    // Check that the sensor data transmission was OK on the client side. 
    assertTrue("Checking send() results", OK);

    // Check that the sensor data appears on the server side. 
    HackystatTestConversation test = new HackystatTestConversation(testUser);
    HackystatTestParameters[] params = {new StartDayTestSelector(testDate),
                                        new SensorDataTypeTestSelector("DevEvent")};
    test.invokeCommand("List Sensor Data", params);
    String typeField = test.getResultCell(1,0);
    assertEquals("Checking SimpleSensor data" + test, testDate.toString(), typeField);    
  }
}

This test case is written using JUnit 4, and so we assume you are familiar with JUnit and the use of annotations like "@Test" to indicate test methods. Note that the Hackystat build system assumes that all JUnit test classes will be named starting with "Test" (which is a hold-over from when we were using JUnit 3.8).

Let's go through the important bits of this test in sections.

ServerProperties serverProperties = ServerProperties.getInstance();    
String testUser = "testsimplesensor";
String host = serverProperties.getHackystatHost();
SimpleSensor sensor = new SimpleSensor(host, testUser + serverProperties.getTestDomain());

These first few lines end with instantiating the SimpleSensor with the "test" host and user key. The "test" host is obtained by creating an instance of the ServerProperties class and called the getHackystatHost() method. When running the unit tests in the Hackystat Framework, this approach is guaranteed to return the test host.

The specification of the test user is also interesting. It turns out that the Hackystat Framework has a kind of "back door" to simplify testing. In normal cases, in order to send sensor data, you must first register with the Hackystat host and have a user key generated and emailed to you. Once you obtain that user key, you can use it to send data. Unfortunately, going through this registration process for every kind of test is complicated and slows down the testing process dramatically. So, the Hackystat Framework has a special case for sensor data sent with a user key that ends with the special Hackystat test domain ("@hackystat.test"). When it receives sensor data with a user key ending in that string, it will immediately register a new account and assign it the user key corresponding to the string with the test domain omitted. So, for example, if a test program sends sensor data to the server using the user key "testsimplesensor@hackystat.test", then the server will automatically create a new account if necessary with the user key "testsimplesensor".

Putting these two pieces of information together, you can see that the result of these first few lines of code is to create a "test" instance of the SimpleSensor which will send data to the local test Hackystat server using the user key "testsimplesensor".

The next few lines of code create a DevEvent using this SimpleSensor instance and send it off to the server.

Date testDate = new Date();
sensor.addDevEvent(testDate, testDate.toString(), System.getProperty("user.dir"));   
boolean OK = sensor.send();

We save the return value from the send() method invocation for use in our first test assertion.

Here's our first JUnit test assertion, which checks to make sure that the sensor data was sent successfully from the perspective of the client.

assertTrue("Checking send() results", OK);

While it is heartening to know that the sensor data was sent successfully from the client-side, it would be even better to be able to verify that we can see the data on the server side. If we were to do this manually, it would involve the following steps:

  1. Bringing up a browser

  2. Navigating to the Hackystat test server home page

  3. Logging in with the test user's user key (in this case, "testsimplesensor")

  4. Clicking on the "Extras" link in the navigation bar to go to the "Extras" page

  5. Entering the current day and "DevEvent" into the "List Sensor Data" command parameters

  6. Invoking the List Sensor Data command

  7. Visually confirming that the specific DevEvent sensor data instance sent by the test case is present in the resulting table of sensor data

After completing all of these steps, we will see a page similar to the one illustrated in Figure 17.3, “ List Sensor Data showing the DevEvent sent by the unit test ”.

Figure 17.3.  List Sensor Data showing the DevEvent sent by the unit test


List Sensor Data showing the DevEvent sent by the unit test

As you can see from the screen image, the unit test did indeed send a DevEvent on the day and time that the test case was run. The problem is that this manual verification is too timeconsuming.

The Hackystat Framework includes a custom testing infrastructure that allows test case developers to programmatically perform the verification we described above in just a few lines of code. This test infrastructure is built on top of the HttpUnit extension to JUnit, and basically adds in specialized knowledge about the Hackystat web application to simplify programmatic manipulation of the web interface.

The final six lines of the testSimpleSensorHappyPath() method illustrates a simple application of the Hackystat test infrastructure to accomplish server-side verification of the SimpleSensor.

HackystatTestConversation test = new HackystatTestConversation(testUser);
HackystatTestParameters[] params = {new StartDayTestSelector(testDate),
                                    new SensorDataTypeTestSelector("DevEvent")};
test.invokeCommand("List Sensor Data", params);
String typeField = test.getResultCell(1,0);
assertEquals("Checking SimpleSensor data" + test, testDate.toString(), typeField);    

The first line creates an instance of a "HackystatTestConversation". This class is similar in spirit to the HttpUnit "WebConversation" class, except that it holds state information regarding an interaction with a Hackystat web application. This constructor is provided with the name of the test user (i.e. "testsimplesensor"). Upon instantiation, this class will find the Hackystat host, attempt to login as the user, and throw an exception if the Hackystat host cannot be found or the test user is not a legal Hackystat account.

The second and third lines create an array of HackystatTestParameters. These are objects that can be used to programmatically specify the values of parameters to Hackystat commands. We create two of these objects, which provide the parameters needed for the List Sensor Data command.

The fourth line calls the invokeCommand() method of our HackystatTestConversation. This method takes a command name ("List Sensor Data") and a set of parameters. It searches the Analyses, Preferences, Alerts, and Extras pages for the command, and if it finds it, invokes the command after setting the parameters appropriately. If the command cannot be found, it throws an Exception.

The fifth line calls the getResultCell() method of our HackystatTestConversation to retrieve the contents of a particular cell in the HTML table returned by the List Sensor Data command.

The sixth and final line calls the JUnit assertEquals() method to check that the data in the cell (which is the "Type" field) matches the data that we sent at the beginning of this test method.

This is just a superficial overview of the Hackystat test infrastructure, which deserves a chapter of its own in the Developers Guide. Until that occurs, you can learn more about the Hackystat test infrastructure by reading the JavaDocs and looking at other examples of the use of the system for testing.

17.6.4. Summary

This section illustrated the enhancement of the SimpleSensor to support testing. To do this, we needed to enhance the local.build.xml file with support for running JUnit tests. We also improved our SimpleSensor class to support an object-oriented API. Finally, we wrote a test class called TestSimpleSensor that runs a single "happy path" test case. By "happy path", we mean that the test case exercises only the expected, normal behavior of the sensor. You will normally want to write additional test cases to check the behavior of your sensor under less than happy conditions.

The next section shows how the SimpleSensor is enhanced to enable downloading and installation via HackyInstaller.