Posts Tagged ‘jdo’

Unit Testing Google Wave Robots

June 18, 2010

I am happy to report that I am now successfully running unit tests on my Wave robot. For this example, I will show how to configure a unit test for a Java robot that responds to the WAVELET_SELF_ADDED event, which is fired when the robot is first added to a wave. My sample robot is designed to do something very simple: when added to a wave, it records the root blip id in an AppEngine data store. Specifically, I’m using JDO to persist objects in the data store.

The configuration comprises:

  • Eclipse with the Google Plug-in. This is not strictly necessary, but you would be crazy to go without it.
  • All of the robot development libraries. The installation of these is covered in the Wave Robots API Java Tutorial. You will need to make sure that gson.jar, oauth.jar, wave-model.jar, and wave-robot-api.jar are on your build path. (I’m skipping the version numbers in the files for clarity.)
  • AppEngine libraries on the build path as described in the Local Unit Testing of the GAE documentation. Two of these were in my google plugin directory but not attached to the project: appengine-api-stubs.jar and appengine-testing.jar. These I put into a lib folder and added to the classpath. Other libraries may be in the war and not yet on the build path, such as appengine-api.jar and appengine-api-labs.jar.

My WaveletRecord class for this example is very simple:

@PersistenceCapable
public class WaveletRecord {
    @SuppressWarnings("unused")
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String rootBlipId;

    public String rootBlipId() { return rootBlipId; }
}

Now, we’re ready to write the unit test. Once again, the GAE/J Local Unit Testing documentation will get us started. We need to have a LocalServiceTestHelper along with setUp and tearDown methods, just like in their example:

public class RobotTest {

    private final LocalServiceTestHelper helper =
        new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());

    @Before
    public void setUp() {
        helper.setUp();
    }

    @After
    public void tearDown() {
        helper.tearDown();
    }

I want to test how my robot responds to onWaveletSelfAdded, so I need a WaveletSelfAddedEvent object. As described in the Debugging Wave Robots article, we can use AppEngine logs to extract the JSON messages that are actually passed between the Wave client and a Robot. In my case, I have just such a String from my logs:

{"events":[{"type":"WAVELET_SELF_ADDED","modifiedBy":"paul.gestwicki@wavesandbox.com","timestamp":1276086048055,"properties":{"blipId":"b+X"}}],"wavelet":{"creationTime":1276086030870,"lastModifiedTime":1276086048055,"version":13,"participants":["paul.gestwicki@wavesandbox.com","cmarnold2@wavesandbox.com","writingthewave@appspot.com"],"participantRoles":{"paul.gestwicki@wavesandbox.com":"FULL","cmarnold2@wavesandbox.com":"FULL","writingthewave@appspot.com":"FULL"},"dataDocuments":{},"tags":[],"creator":"paul.gestwicki@wavesandbox.com","rootBlipId":"b+X","title":"Bugs squashed","waveId":"wavesandbox.com!w+JkmJGit7A","waveletId":"wavesandbox.com!conv+root"},"blips":{"b+X":{"annotations":[{"name":"conv/title","value":"","range":{"start":0,"end":14}},{"name":"lang","value":"en","range":{"start":0,"end":14}},{"name":"lang","value":"en","range":{"start":15,"end":431}}],"elements":{"0":{"type":"LINE","properties":{}},"14":{"type":"LINE","properties":{}},"15":{"type":"LINE","properties":{}}},"blipId":"b+X","childBlipIds":[],"contributors":["paul.gestwicki@wavesandbox.com"],"creator":"paul.gestwicki@wavesandbox.com","content":"Nothing to see here, please move along.","lastModifiedTime":1276086031862,"parentBlipId":null,"version":8,"waveId":"wavesandbox.com!w+JkmJGit7A","waveletId":"wavesandbox.com!conv+root"}},"robotAddress":"writingthewave@appspot.com"}

Of course, we can’t just put that into our Java code directly: we have to escape all of the quotation marks to make it a valid String literal. However, once we do that, we can make a handy constant like this:

private static final String SELF_ADDED_EVENT = "{\"events\":[{\"type\":\"WAVELET_SELF_ADDED\",\"modifiedBy\":\"paul.gestwicki@wavesandbox.com\",\"timestamp\":1276086048055,\"properties\":{\"blipId\":\"b+X\"}}],\"wavelet\":{\"creationTime\":1276086030870,\"lastModifiedTime\":1276086048055,\"version\":13,\"participants\":[\"paul.gestwicki@wavesandbox.com\",\"cmarnold2@wavesandbox.com\",\"writingthewave@appspot.com\"],\"participantRoles\":{\"paul.gestwicki@wavesandbox.com\":\"FULL\",\"cmarnold2@wavesandbox.com\":\"FULL\",\"writingthewave@appspot.com\":\"FULL\"},\"dataDocuments\":{},\"tags\":[],\"creator\":\"paul.gestwicki@wavesandbox.com\",\"rootBlipId\":\"b+X\",\"title\":\"Bugs squashed\",\"waveId\":\"wavesandbox.com!w+JkmJGit7A\",\"waveletId\":\"wavesandbox.com!conv+root\"},\"blips\":{\"b+X\":{\"annotations\":[{\"name\":\"conv/title\",\"value\":\"\",\"range\":{\"start\":0,\"end\":14}},{\"name\":\"lang\",\"value\":\"en\",\"range\":{\"start\":0,\"end\":14}},{\"name\":\"lang\",\"value\":\"en\",\"range\":{\"start\":15,\"end\":431}}],\"elements\":{\"0\":{\"type\":\"LINE\",\"properties\":{}},\"14\":{\"type\":\"LINE\",\"properties\":{}},\"15\":{\"type\":\"LINE\",\"properties\":{}}},\"blipId\":\"b+X\",\"childBlipIds\":[],\"contributors\":[\"paul.gestwicki@wavesandbox.com\"],\"creator\":\"paul.gestwicki@wavesandbox.com\",\"content\":\"Nothing to see here, please move along.\",\"lastModifiedTime\":1276086031862,\"parentBlipId\":null,\"version\":8,\"waveId\":\"wavesandbox.com!w+JkmJGit7A\",\"waveletId\":\"wavesandbox.com!conv+root\"}},\"robotAddress\":\"writingthewave@appspot.com\"}";

Yes, it’s ugly, but it allows us now to easily create a real WaveletSelfAddedEvent using Google’s gson library, which you have as part of the robot development libraries. We just need to make a GSON factory, parse the JSON, and pull out our event from the bundle. That is,

GsonFactory gsonFactory = new GsonFactory();
Gson gson = gsonFactory.create();
EventMessageBundle bundle = gson.fromJson(SELF_ADDED_EVENT, EventMessageBundle.class);
WaveletSelfAddedEvent event = (WaveletSelfAddedEvent) bundle.getEvents().get(0);

Now, unlike in the GAE/J Local Unit Testing approach, I am going to use JDO to check that my robot is behaving as expected. Will this work? Honestly, it surprised me, but it sure does: configuring the LocalServiceTestHelper was enough to allow us to get an appropriate PersistenceManager using the usual techniques.

The following code will create for us a query that will return all WaveletRecord objects:

PersistenceManager pm = PMF.get().getPersistenceManager();
Query query = pm.newQuery(WaveletRecord.class);

We will also have a sanity check to ensure that there is nothing there yet.

List records = (List) query.execute();
assertEquals("We should start out with nothing in the datastore.", 0, records.size());

Running the robot is easy as pie once we know what we’re doing. My robot is implemented as DataCollectorServlet, so I can poke it and see how it reacts to being added to a wave.

DataCollectorServlet robot = new DataCollectorServlet();
robot.onWaveletSelfAdded(event);

How do we know if this worked? If there is now a WaveletRecord in the datastore and its root blip ID matches the one in the event. Notice that I don’t have to hardcode that root blip ID anywhere: I’ll just pull it out of the event. The event is not a “mock object”: it’s a real event as Wave would generate, but it has all the nice lightweightness of a mock object.

records = (List) pm.newQuery(WaveletRecord.class).execute();
assertEquals("There should be exactly one record returned by the query.", 1, records.size());
WaveletRecord waveletRecord = records.get(0);
assertEquals("The rootblip id of the single wavelet record should match the one in the event.",
  event.getWavelet().getRootBlipId(), waveletRecord.rootBlipId());

That’s it! The unit test passes, and now test-driven development can commence.

(Turns out my robot actually writes a blip on being added, too, but testing that is left as an exercise for the reader.)