Friday 29 November 2013

Unit Testing / TDD - continuing the tests for on() and trigger()

G'day:
I was getting into a rhythm with my TDD cycle this afternoon... test... refine... test... refine... here's what I was doing. Well: after the obligatory recap links (/SEO bait):

Do you know what? I am actually enjoying writing this code. And I'm now past the bits I already knew I had to write (and had the code pretty much already written in my head), so I'm doing really really TDD now. On with the show...

Update:

This article had been entitled "Completing the tests for on() and trigger()". That was from an earlier - more ambitious - draft. I do not finish the tests for on() and trigger() in this article. I only get about half way, I reckon. I have updated the title (but not the URL) accordingly.

In the last entry I finally got on() and trigger() actually doing what they were supposed to do (in a very limited way), which is good. However they only work with a single event. Time to sort that out.

Here's the test for handling more than one event:

public void function testOnStoresMultipleEvents() {
     structDelete(variables, "testOnStoresMultipleEvents1"); // ensure it doesn't exist
     structDelete(variables, "testOnStoresMultipleEvents2"); // ensure it doesn't exist
     var handlerResponse1 = createUuid();
     var handlerResponse2 = createUuid();

     variables.eventObject.on("testOnStoresMultipleEventsEvent1Run", function(){
          variables.testOnStoresMultipleEvents1 = handlerResponse1;
     });
     variables.eventObject.on("testOnStoresMultipleEventsEvent2Run", function(){
          variables.testOnStoresMultipleEvents2 = handlerResponse2;
     });

     variables.eventObject.trigger("testOnStoresMultipleEventsEvent1Run");

     assertTrue(
          structKeyExists(variables, "testOnStoresMultipleEvents1"),
          "testOnStoresMultipleEvents1 should have been created"
     );

     assertEquals(
          handlerResponse1,
          variables.testOnStoresMultipleEvents1,
          "testOnStoresMultipleEvents1 set incorrectly"
     );

     variables.eventObject.trigger("testOnStoresMultipleEventsEvent2Run");
     assertTrue(
          structKeyExists(variables, "testOnStoresMultipleEvents2"),
          "testOnStoresMultipleEvents2 should have been created"
     );

     assertEquals(
          handlerResponse2,
          variables.testOnStoresMultipleEvents2,
          "testOnStoresMultipleEvents2 set incorrectly"
     );
}

All we're doing here is doubling up the previous test: adding handlers for two events instead of one, and making sure each do the thing they're supposed to.

And the code change for this is as follows:

function createEventObject(){
     var eventContainer = {};
     return {
          on = function(required string event, required function handler){
               eventContainer[event] = arguments;
          },
          trigger = function(required string event){
               eventContainer[event].handler();
          }
     };
}

All I've done here is to make the eventContainer be a struct which is keyed on the event name, instead of just containing the one event handler. I think it's safe to say that one could predict that the previous iteration of this code was never going to see production - it only handled one event and one handler - but this is OK: what we were able to test is that a handler was being "registered", and having done that it could then be called when the event for it was triggered. This is a win. and from there we're just changing the data structuring hold in the registered event handlers to accommodate more than one event (and shortly, more than one handler per event).

Before we do that though, I've re-organised my test files a bit. I used to have one big test CFC, but it was getting big and cluttered, so I've divvied it up a bit. I now have a test CFC for the basic tests of the createEventObect() function's tests, another for the specific on() tests, and another for the trigger() tests. And "beneath" all that a baseline CFC which sets the test environment up. It's just a refactoring exercise to make my files and my tests more coherently organised. Here's all the code:

// TestBase.cfc
component extends="mxunit.framework.TestCase" {

     public void function beforeTests(){
          addAssertDecorator("CustomAssertions");
          include "./function.cfm";
     }

     public void function setup(){
          variables.eventObject = createEventObject();
     }

}

There's no tests in TestBase.cfc, I'm just setting the baseline environment up. The other CFCs extend this one, to inherit that baseline.

// TestCreateEventObject.cfc
component extends="TestBase" {

     public void function testBaseline(){
          createEventObject();
          // it doesn't error. That's it
     }

     public void function testReturnValues(){
          assertIsStruct(variables.eventObject, "Returned value should be a struct");
          assertStructKeysCorrect(
               "on,trigger",
               variables.eventObject,
               "Incorrect keys returned in eventObject"
          );

          assertTrue(
               isClosure(variables.eventObject.on),
               "The returned on() value should be a function"
          );

          assertTrue(
               isClosure(variables.eventObject.trigger),
               "The returned trigger() value should be a function"
          );
     }

     public void function testOnRequiresEventArg() {
          var failMsg = "on() should require an EVENT argument";
          try {
               variables.eventObject.on(handler=function(){});
               fail(failMsg);
          } catch (any e){
               // can't catch this coherently by exception type as CF and Railo return completely different exceptions here
               assertTrue(
                    findNoCase("event", e.message) && findNoCase("parameter", e.message),
                    failMsg
               );
          }
     }

     public void function testOnRequiresHandlerArg()  {
          var failMsg = "on() should require an HANDLER argument";
          try {
               variables.eventObject.on(event="TestEvent");
               fail(failMsg);
          } catch (any e){
               assertTrue(
                    findNoCase("handler", e.message) && findNoCase("parameter", e.message),
                    failMsg
               );
          }
     }

     public void function testTriggerRequiresEventArg()  {
          var failMsg = "trigger() should require an EVENT argument";
          try {
               variables.eventObject.trigger();
               fail(failMsg);
          } catch (any e){
               assertTrue(
                    findNoCase("event", e.message) && findNoCase("parameter", e.message),
                    failMsg
               );
          }
     }

}

TestCreateEventObject.cfc tests the returned struct from createEventObject(), the method signatures of on() and trigger(), but not the behaviour of either of those functions.

// TestOn.cfc
component extends="TestBase" {

     public void function testOnStoresHandler() {
          structDelete(variables, "testOnStoresHandlerResponse"); // ensure it doesn't exist
          var handlerResponse = createUuid();

          variables.eventObject.on("TestOnStoresHandlerRun", function(){
               variables.testOnStoresHandlerResponse = handlerResponse;
          });
          variables.eventObject.trigger("TestOnStoresHandlerRun");

          assertTrue(
               structKeyExists(variables, "testOnStoresHandlerResponse"),
               "testOnStoresHandlerResponse should have been created"
          );

          assertEquals(
               handlerResponse,
               variables.testOnStoresHandlerResponse,
               "testOnStoresHandlerResponse set incorrectly"
          );
     }

     public void function testOnStoresMultipleEvents() {
          structDelete(variables, "testOnStoresMultipleEvents1"); // ensure it doesn't exist
          structDelete(variables, "testOnStoresMultipleEvents2"); // ensure it doesn't exist
          var handlerResponse1 = createUuid();
          var handlerResponse2 = createUuid();

          variables.eventObject.on("TestOnStoresMultipleEventsEvent1Run", function(){
               variables.testOnStoresMultipleEvents1 = handlerResponse1;
          });

          variables.eventObject.on("TestOnStoresMultipleEventsEvent2Run", function(){
               variables.testOnStoresMultipleEvents2 = handlerResponse2;
          });

          variables.eventObject.trigger("TestOnStoresMultipleEventsEvent1Run");
          assertTrue(
               structKeyExists(variables, "testOnStoresMultipleEvents1"),
               "testOnStoresMultipleEvents1 should have been created"
          );

          assertEquals(
               handlerResponse1,
               variables.testOnStoresMultipleEvents1,
               "testOnStoresMultipleEvents1 set incorrectly"
          );

          variables.eventObject.trigger("TestOnStoresMultipleEventsEvent2Run");
          assertTrue(
               structKeyExists(variables, "testOnStoresMultipleEvents2"),
               "testOnStoresMultipleEvents2 should have been created"
          );

          assertEquals(
               handlerResponse2,
               variables.testOnStoresMultipleEvents2,
               "testOnStoresMultipleEvents2 set incorrectly"
          );
     }

}

TestOn.cfc has the tests we've written in the last article and so far in this one.

// TestTrigger.cfc
component extends="TestBase" {

     // currently trigger() tests are actually performed as part of the on() tests.
     public void function beforeTests(){
          super.beforeTests();
          testOn = new TestOn();
          testOn.beforeTests();
     }

     public void function setup(){
          super.setup();
          testOn.setup();
     }

     // TODO: find a way to decouple these tests
     public void function testTriggerCalledStoredHandler() {
          testOn.testOnStoresHandler();
     }

     public void function testTriggerCalledMultipleStoredEvents() {
          testOn.testOnStoresMultipleEvents();
     }

}

For the sake of completeness, I have also implemented a TestTrigger.cfc. Currently all this CFC has is placeholder tests which simply call the on() tests that use trigger(). This is a bit of an code execution double-up, but when we run our battery of tests, we get flagged up that trigger()'s behaviour is actually being tested, if only by proxy via other tests.

Because for the trigger() tests to work I need to call the associated on() test, I need to actually instantiate the TestOn.cfc and run its test methods directly. And to do that I have to call its beforeTests() and setup() too before each trigger() test. This is quite an "interesting" approach I'm taking here, and not one I've tried (or needed to try) before.

So now if I kick off the test run, I get the following results:



Round about now a missing test case occurred to me. It's all well and good testing trigger() triggering events we've got handlers registered for, but I could see we'd be in trouble if we triggered an event that had no handlers registered:

trigger = function(required string event){
     eventContainer[event].handler();
}

That's fine if there is an eventContainer[event], but this is only the case if the event event has had a handler bound to it with on(). I'll demonstrate this:

public void function testTriggerNonExistentEvent() {
     variables.eventObject.trigger("EventWithNoHandlers");
     // the test is that it doesn't error
}

But of course it does error:



That's cool... the fix is easy: we just test that there's a handler registered for that event before we try to run it. CFML 101 sort of stuff:

trigger = function(required string event){
     if (structKeyExists(eventContainer, event)){
          eventContainer[event].handler();
     }
}

Now when we run our testTriggerNonExistentEvent() test, it's green. And we can run the rest of the tests and there's no regressions from this, as all the tests are green. Here's the cool thing about an iterative TDD approach: we find bugs... or lack thereof as we go. I know that the change I just made does not interfere with any other functionality. The code is solid (at what it sets out to do).

Note: it's completely OK that when an event is triggered that simply nothing happens if there are no handlers bound to the event. This is what we want. If a tree falls in a forest and no-one is there to listen to it, did it make any noise? In this case: we don't care. Sorry tree.

The next shortfall is that one can only register a single handler for a given event:

public void function testOnStoresMultipleHandlers() {
     structDelete(variables, "TestOnStoresMultipleHandlers1"); // ensure it doesn't exist
     structDelete(variables, "TestOnStoresMultipleHandlers2"); // ensure it doesn't exist
     var handlerResponse1 = createUuid();
     var handlerResponse2 = createUuid();

     variables.eventObject.on("TestOnStoresMultipleHandlers", function(){
          variables.testOnStoresMultipleHandlers1 = handlerResponse1;
     });

     variables.eventObject.on("TestOnStoresMultipleHandlers", function(){
          variables.testOnStoresMultipleHandlers2 = handlerResponse2;
     });

     variables.eventObject.trigger("TestOnStoresMultipleHandlers");
     assertTrue(
          structKeyExists(variables, "testOnStoresMultipleHandlers1"),
          "testOnStoresMultipleHandlers1 should have been created"
     );

     assertEquals(
          handlerResponse1,
          variables.testOnStoresMultipleHandlers1,
          "testOnStoresMultipleHandlers1 set incorrectly"
     );

     assertTrue(
          structKeyExists(variables, "testOnStoresMultipleHandlers2"),
          "testOnStoresMultipleHandlers2 should have been created"
     );

     assertEquals(
          handlerResponse2,
          variables.testOnStoresMultipleHandlers2,
          "testOnStoresMultipleHandlers2 set incorrectly"
     );

}

Previously we bound a single handler to each of a coupla events (see testOnStoresMultipleEvents()). This is different: here we just trigger the one event, but we have first bound two separate event handlers to it. But we still expect them to both fire. Obviously currently they don't: our eventContainer is a struct keyed on event name (so multiple events are possible), but each struct element is a single value holding a single handler. No good. When we run this test it fails. But again, the fix is an easy one: change eventContainer[event] to be an array, and when we call on() we append to that array. This time we need to adjust both on() and trigger():

function createEventObject(){
     var eventContainer = {};
     return {
          on = function(required string event, required function handler){
               if (!structKeyExists(eventContainer, event)){
                    eventContainer[event] = [];
               }
               arrayAppend(eventContainer[event], arguments);
          },
          trigger = function(required string event){
               if (structKeyExists(eventContainer, event)){
                    for (eventEntry in eventContainer[event]){
                         eventEntry.handler();
                    }
               }
          }
     };
} 

This still happily supports a single event with a single handler, and multiple events with a single handler. Now it also supports multiple events each with multiple handlers. We know this, because all our tests are green.

Oh, although this is another case of there being no isolated way of separating the test of on() and trigger() here, I've done the main testing in TestOn.cfc, but I have also added a stub test in TestTrigger.cfc too:

public void function testTriggerCalledMultipleStoredHandlers() {
     testOn.testOnStoresMultipleHandlers();
}

That seems like a suitable juncture to finish up this article. This code is now functional for dealing with as many events as we like, with as many event handlers on each event as we like. So this is actually usable code now! However we also need to be able to pass data along with the handler and the trigger(), which we'll implement next.

All the code for this will be committed to github, although I have to admit I'm a bit further ahead with the code than I am with the blog, so it's current state has code for the next article in it.

I'll get cracking with the next article soon. I'm over in Galway this weekend, so I do have plenty of time to spare between my visits to see my son. And the rugby season has finished (the All Blacks finished 2013 going unbeaten... did I tell you that? Indeed they've only lost one game since the last World Cup, whilst I'm talking about them. One loss, one draw, and something like 24 wins I think. They are a truly awesome rugby team). So, anyway, no rugby to watch so I'll probably focus on the blog more than I have been for the last coupla weekends.

Righto.

--
Adam