15.8. Anatomy of SimpleSdt: the SDT Unit Tests

Once you have implemented the XML definition file, the wrapper class, and the ShellCommand class, it is time to develop the test cases that ensure that your sensor data type works as intended. (Of course, if you are using a Test Driven Design strategy, you might want to implement this class first. Whatever works for you.)

Testing an SDT consists of two primary cases: (1) Tests to make sure that the sensor data instance of this type can be constructed correctly from entryattribute values, and (2) Tests to make sure that a sensor data instance of this type can be sent to the server correctly. Example 15.6, “The org.hackystat.doc.simplesdt.TestSimpleSdt class” illustrates the test class for SimpleSdt, again with import statements and JavaDocs ommitted for space reasons.

Example 15.6. The org.hackystat.doc.simplesdt.TestSimpleSdt class

package org.hackystat.doc.simplesdt;

public class TestSimpleSdt extends TestCase {
  
  private String sensorType = "SimpleSdt";
  private String tool = "TimerTool";
  private Date timestamp = new Date();
  private String fileName = "foo/bar/Baz.java";
  private String elapsedTimeString = "1000";
  private String macOSX = "Mac OS/X";
  private SensorDataPropertyMap pMap = new SensorDataPropertyMap();
  private Map keyValStringMap = new HashMap();
  
  public void testSDT() throws Exception {
    keyValStringMap.clear();
    keyValStringMap.put("tool", tool);
    keyValStringMap.put("tstamp", String.valueOf(timestamp.getTime()));
    keyValStringMap.put("fileName", fileName);
    keyValStringMap.put("elapsedTime", elapsedTimeString);
    pMap.put("environment", macOSX);
    keyValStringMap.put("pMap", pMap.encode());
    SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry(sensorType, keyValStringMap);
    assertEquals("Checking timestamp", timestamp, entry.getTimestamp());
    assertEquals("Checking tool", tool, entry.getTool());
    assertEquals("Checking fileName", fileName, entry.getFileName());
    assertEquals("Checking elapsedTime", 1000, entry.getElapsedTime());
    assertEquals("Checking environment", macOSX, entry.getProperty("environment"));
  }


  public void testGetErrors() throws Exception {
    // Check that a missing fileName entryAttribute throws an exception
    keyValStringMap.clear();
    keyValStringMap.put("tool", tool);
    keyValStringMap.put("tstamp", String.valueOf(timestamp.getTime()));
    keyValStringMap.put("elapsedTime", elapsedTimeString);
    keyValStringMap.put("pMap", pMap.encode());
    try {
      SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry(sensorType, keyValStringMap);
      fail("Missing fileName did not throw an exception.");
    }
    catch (Exception e) {
      // all good.
    }

    // Check that a negative elapsedTime value throws an exception.
    keyValStringMap.put("elapsedTime", "-1");
    try {
      SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry(sensorType, keyValStringMap);
      fail("Negative elapsedTime did not throw an exception.");
    }
    catch (Exception e) {
      // all good.
    }
  }
  public void testShellCommand() throws Exception {
    ServerProperties serverProperties = ServerProperties.getInstance();    
    String testUser = "testshellcommand" + serverProperties.getTestDomain();
    String host = serverProperties.getHackystatHost();
    SensorProperties sensorProps = new SensorProperties(host, testUser);

    boolean isInteractive = false;
    boolean enableOfflineData = false;
    SensorShell shell = new SensorShell(sensorProps, isInteractive, tool, enableOfflineData);
    
    String[] addArgs = {
        "add", 
        "fileName=" + fileName, 
        "tool=" + tool,
        "elapsedTime=" + elapsedTimeString,
        "pMap=" + pMap.encode()
        };
    
    assertTrue("Checking add of SimpleSdt", shell.doCommand(timestamp, "SimpleSdt", addArgs));
    assertTrue("Checking send", shell.send());
  }
}

As is typically the case with test code, this class is valuable not only for its ability to diagnose errors in the implementation, but also for the way it illustrates aspects of the sensor data type API, which is very useful for developers of sensors who wish to create instances of this type of sensor data. Let's now look at the various components of this test class in a little more detail.

15.8.1. Test case declaration and setup

The first part of the test class declares the class and creates sample sensor data values for use in testing, as illustrated below.

public class TestSimpleSdt extends TestCase {
  
  private String sensorType = "SimpleSdt";
  private String tool = "TimerTool";
  private Date timestamp = new Date();
  private String fileName = "foo/bar/Baz.java";
  private String elapsedTimeString = "1000";
  private String macOSX = "Mac OS/X";
  private SensorDataPropertyMap pMap = new SensorDataPropertyMap();
  private Map keyValStringMap = new HashMap();

First, note that the class must be named beginning with "Test". This is a convention in the Hackystat build system which uses "Test*" as a regular expression to detect which classes should be passed to JUnit as test code. Also, it is necessary that the test class extend junit.framework.TestCase.

The remainder of the above code fragment simply sets up some instance variables to use as example data values in the test methods.

15.8.2. Testing sensor data instance construction

It is important to test that an instance of the sensor data wrapper class can be constructed by the system given correct entryattribute values, and that the entryattribute values can be retrieved correctly from the wrapper class API. The testSdt method shows how to implement this test, as illustrated below. The method body is separated into three sections, divided by empty lines.

  public void testSDT() throws Exception {
    keyValStringMap.clear();
    keyValStringMap.put("tool", tool);
    keyValStringMap.put("tstamp", String.valueOf(timestamp.getTime()));
    keyValStringMap.put("fileName", fileName);
    keyValStringMap.put("elapsedTime", elapsedTimeString);
    pMap.put("environment", macOSX);
    keyValStringMap.put("pMap", pMap.encode());

    SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry(sensorType, keyValStringMap);

    assertEquals("Checking timestamp", timestamp, entry.getTimestamp());
    assertEquals("Checking tool", tool, entry.getTool());
    assertEquals("Checking fileName", fileName, entry.getFileName());
    assertEquals("Checking elapsedTime", 1000, entry.getElapsedTime());
    assertEquals("Checking environment", macOSX, entry.getProperty("environment"));
  }

The first section in this method sets up the keyValStringMap and pMap data structures with the five entryattributes associated with SimpleSdt: the two explicitly declared entryattributes (fileName and elapsedTime), and the three implicitly declared entryattributes (tool, tstamp, and pMap). Note that all of the keys and all of the values in the keyValStringMap must be Strings, which is why we convert the timestamp Date instance to a String using the valueOf method and the pMap to a String using its encode method. We also add an optional key-value mapping ("environment", "Mac OS/X") to the pMap to ensure that this mechanism functions correctly.

The second section in this method consists of just one line, which creates an instance of the SimpleSdt wrapper class from the data values we just set up. This requires invoking SensorDataEntryFactory.getEntry, passing it a String that identifies the SDT to be created ("SimpleSdt"), and the mapping of key-value pairs representing the entryattribute names and values. If the sensor data instance can be constructed successfully, this method returns the instance. If problems occur, such as the getErrors() method in the wrapper class returning a non-null value, then an Exception is thrown by this method.

The final section in this method is executed only if the SimpleSdt instance was instantiated successfully. This part of the test checks that the data values passed in for instance creation can now be retrieved correctly from the SimpleSdt interface. Note that while certain fields (tstamp, elapsedTime) are passed as Strings to the instance creation mechanism, they are retrieved from the API in their "converted" form (tstamp is retrieved as a Date, and elapsedTime is retrieved as an int).

15.8.3. Testing illegal sensor data instance construction

Each wrapper class must implement a getErrors() method, which checks to ensure that the entryattributes provided during construction are semantically meaningful. In the case of SimpleSdt, the getErrors method checks to make sure that the fileName is not null and that elapsedTime is not negative.

The testGetErrors method makes sure that the sensor data instance creation mechanism does indeed throw an exception under these conditions, as illustrated below:

  public void testGetErrors() throws Exception {
    // Check that a missing fileName entryAttribute throws an exception
    keyValStringMap.clear();
    keyValStringMap.put("tool", tool);
    keyValStringMap.put("tstamp", String.valueOf(timestamp.getTime()));
    keyValStringMap.put("elapsedTime", elapsedTimeString);
    keyValStringMap.put("pMap", pMap.encode());
    try {
      SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry(sensorType, keyValStringMap);
      fail("Missing fileName did not throw an exception.");
    }
    catch (Exception e) {
      // all good.
    }

    // Check that a negative elapsedTime value throws an exception.
    keyValStringMap.put("elapsedTime", "-1");
    try {
      SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry(sensorType, keyValStringMap);
      fail("Negative elapsedTime did not throw an exception.");
    }
    catch (Exception e) {
      // all good.
    }
  }

15.8.4. Testing sensor data transmission to the server

The first two test methods verified that sensor data instances can be constructed correctly and that the validity checking mechanism works as desired. The final test case checks that a sensor data instance of the specified type, once created, can be sent to the server. The testShellCommand method performs this test, as illustrated below.

  public void testShellCommand() throws Exception {
    ServerProperties serverProperties = ServerProperties.getInstance();    
    String testUser = "testshellcommand" + serverProperties.getTestDomain();
    String host = serverProperties.getHackystatHost();
    SensorProperties sensorProps = new SensorProperties(host, testUser);

    boolean isInteractive = false;
    boolean enableOfflineData = false;
    SensorShell shell = new SensorShell(sensorProps, isInteractive, tool, enableOfflineData);
    
    String[] addArgs = {
        "add", 
        "fileName=" + fileName, 
        "tool=" + tool,
        "elapsedTime=" + elapsedTimeString,
        "pMap=" + pMap.encode()
        };
    
    assertTrue("Checking add of SimpleSdt", shell.doCommand(timestamp, "SimpleSdt", addArgs));
    assertTrue("Checking send", shell.send());
  }

This method has several parts. The first part constructs a special "test" instance of SensorProperties. Under normal conditions, the SensorShell will look in the .hackystat directory for a sensor.properties file and construct a SensorProperties instance from the values it finds there. In the case of test code, we don't want to use those values, so instead we need to construct a special version of the SensorProperties instance and use a version of the SensorProperties constructor that allows us to pass in a SensorProperties instance explicitly. Our special version points the SensorShell to the test host (retrieved from the hackystat.site.properties file) and to a special user (testshellcommand@hackystat.test). The use of a special ""testdomain" allows us to send data to an account without registering it first; the server will register it implicitly and also set the user key to be equal to the account name (in this case, "testshellcommand").

The second part constructs an instance of the SensorShell. In this case, we use a version of the SensorShell constructor that allows us to pass in the SensorProperties instance explicitly, and also indicate that if the SensorShell cannot connect to the test host, it should not store the data offline.

The third part of this method constructs an array of Strings that will be used to invoke a command in the SensorShell. Recall from Example 15.5, “SensorShell Interactive Session” that one can interactively invoke a SensorShell command using "#" delimited syntax, such as in the following example:

SimpleSdt#add#fileName=c:\foo\bar\Zob.c#elapsedTime=25#tool=SensorShell

To perform the same function using the programmatic API, we construct a String array containing all of the arguments following the command name ("SimpleSdt"):

    String[] addArgs = {
        "add", 
        "fileName=" + fileName, 
        "tool=" + tool,
        "elapsedTime=" + elapsedTimeString,
        "pMap=" + pMap.encode()
        };

We can then invoke the command using the doCommand method, passing it the command name and the associated arguments:

shell.doCommand(timestamp, "SimpleSdt", addArgs)

The doCommand method returns a boolean indicating whether the command succeeded or not, so we can wrap this code in an assert clause to cause the unit test to fail if this command invocation did not succeed.

Finally, we can send this data to the server by invoking the SensorShell's send method:

shell.send()

Once again, this method returns a boolean indicating its success, which we can check via a JUnit assert clause.