Monday, 18 November 2013

Unit Testing / TDD - more tests, more development

G'day:
So enough prattling about <cfclient> for the time being (and, no, I haven't finished with that topic, but Adobe have demonstrated themselves up to the task of ridiculing themselves without my help at the moment, so I'm just leaving them to it). Back to something more useful: TDD / unit testing.

So far I've written four articles in this series:

Only the third of which had any code in it: the rest was just general discussion. Today I'm going to continue developing this createEventObject() idea I've had, using TDD.

So far I've got to the point where I have this test:

public void function baseline(){
    createEventObject();
}

And I have written enough code to make it pass, but only enough code to make it pass:

function createEventObject(){};

This seems like an infeasibly small step, but TDD is an iterative process, so one needs to start somewhere. Also bear in mind that I'm basically writing both sets of code at much the same time. The process was this:
  1. I'm gonna write a function.
  2. I'll need tests (creates Test.cfc).
  3. Deliberation as to what to call the function.
  4. I'm gonna call the function "createEventObject()".
  5. Writes a test to call createEventObject().
  6. Writes function called createEventObject().
Normally I would not be writing  a 3500-word blog article around those steps: it would only represent about 5min planning / work. I feel I need to stress this, because some of my readers are already thinking all this sounds like too much work, but it's hardly any more work than not doing it, and it helps build a rhythm. Also, I'd not simply sit back and go "tahdah!" when I got to step six, I'd just start straight away planning what comes next with this function... does it need inputs? Does it have outputs? And accordingly the next tests are already forming in my head, and those tests will prove than once I write the code, I've done it right. It's the first step in a longer journey (you know: "all journeys begin with a single step", etc).

OK... but in reality... I've not thought about this function for a week or so now, I need to get my head back into it:
Similar to my code deferral function from a coupla weeks ago ("Threads, callbacks, closure and a pub"), [...] I started to wonder if I could write an event-driven system in a single function too. [...] My theory is I should be able to have a function which returns a coupla other functions which handle event triggering (trigger()) and listening (on()), and because they'll implement closure, they'll remember the main function's variable context, so will both be able to communicate with an central event registry.

The next thing that this function needs to do is return a struct containing two functions: trigger() and on(). I suspect I need one or two more functions coming back from this, but for the initial iterations we can definitely agree I need to be able to add a handler for an event (on()), and also be able to fire the event (trigger()). We're not achieving anything if we don't have at least those.

So here's a test that verifies functions on() and trigger() are returned:

public void function testReturnValues(){
    var eventObject = createEventObject();
    
    assertIsStruct(eventObject, "Returned value should be a struct");
    assertStructKeysCorrect(
        "on,trigger",
        eventObject,
        "Incorrect keys returned in eventObject"
    );
    assertTrue(
        isClosure(eventObject.on),
        "The returned on() value should be a function"
    );
    assertTrue(
        isClosure(eventObject.trigger),
        "The returned trigger() value should be a function"
    );
}

I have four assertions here which fulfil the requirement for this code:
  • that the returned value is a struct;
  • that the struct contains only "on" and "trigger" keys (nothing extraneous);
  • that each of the returned key values are references to "closures" (see below for my opinion of this).
assertIsStruct() and assertTrue() are both built-in to MXUnit ("Built-in assertions"). However I've slipped a custom one in there as well: assertStructKeysCorrect(). The chief reason I did this was to demonstrate how custom assertions work.

I've added this file to the mix:

// CustomAssertions.cfc
component {

    public void function assertStructKeysCorrect(required string keys, required struct struct, string message){
        var assertionArgs = {
            expected = listSort(keys, "textnocase"),
            actual     = listSort(structKeyList(struct), "textnocase")
        };
        if (structKeyExists(arguments, "message")){
            assertionArgs.message = message;
        }
        assertEquals(argumentCollection=assertionArgs);
    }

}

And am calling it in via beforeTests() in Test.cfc:

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

Custom assertions are fairly straight forward, and are documented here: "Custom Assertions".

Perhaps we're gettting ahead of ourselves... I'm discussing in-built assertions and custom assertions, but I've not really mentioned what the hell is an assertion?

An assertion is basically just a function that throws an exception if a condition is not met. It's explained better than I can do it on Wikipedia: "Assertion (software development)". But what we're basically doing in a unit test is going:
Right, well I've run some code. As a result of that I am going to make some assertions based on what I expect to have happened as a result of that code having been run. For example a variable might have changed, a result might have been returned, one thing might now be equal to another thing, etc.
For example if I run a function called multiply() which takes two arguments, I can assert that the result ought to be the product of those two arguments:

assertEquals(42, multiply(6,7));

Because 6 x 7 is indeed 42, nothing happens. Code keeps executing. If the assertion being made is fulfilled, processing continues.

However if we had this:

assertEquals(54, multiply(8,7));

assertEquals() will raise an exception. In particular an mxunit.exception.AssertionFailedError. The way the MXUnit test environment works is that it catches AssertionFailedErrors, and reports that as a failed test. IE: the page doesn't simply error out.




Any other sort of exception is caught and reported as an errored test. For example if I had a sloppily written divide() function:

function divide(x,y){
    return x/y;
}
And then tested it with some data known to cause it to fail:


public void function testDivideWithZeroDenominator(){
    assertEquals(
        0,
        divide(13,0)
    );
}
public void function testDivideWithStrings(){
    assertEquals(
        0,
        divide("x","y")
    );
}

We don't just get MXUnit erroring out, we get this:



I'm doing equality assertions on all of these, but there's a bunch of different ones that ship with MXUnit, eg:
  • assert() / assertTrue() - takes an expression which one is asserting should be true
  • assertFalse() - as above, but expecting false
  • assertIsStruct() - the specified value is a struct
  • fail() - just makes the test fail (this is handy if you're writing up skeletons for tests you need to write, but aren't writing yet: remember a test ought to - to start with - fail).
And, obviously, as I indicate above one can also create custom assertions easily enough.

So if we go back to my code above now:

public void function testReturnValues(){
    var eventObject = createEventObject();
    
    assertIsStruct(eventObject, "Returned value should be a struct");
    assertStructKeysCorrect(
        "on,trigger",
        eventObject,
        "Incorrect keys returned in eventObject"
    );
    assertTrue(
        isClosure(eventObject.on),
        "The returned on() value should be a function"
    );
    assertTrue(
        isClosure(eventObject.trigger),
        "The returned trigger() value should be a function"
    );
}

We've got some code we're putting to the test, and we are making four assertions about that code. We've got a bit of logic hidden away in custom assertion, but the test itself is very simple, basically saying (as noted earlier):
The eventObject returned from createEventObject() must be: 
  • a struct
  • have keys on and trigger and nothing else
  • the on key's value must be a function which implements closure
  • as does trigger
Simple! This is one thing to try to strive for in ones tests: keep 'em simple.

This article was a bit short, but we added in one new unit test, had a look at what an assertion is, and why, added a custom assertion and had a look at how simple our test code can be.

I've got some more tests already written for this work, and will follow-up with another article shortly. I'm a bit time-constrained at the moment, so I want to keep the articles shorter, so that I'm more likely to get them written and out the door.

--
Adam