Wednesday 20 November 2013

Unit Testing / TDD - getting stuck on how / what to test (part 1/2)

G'day:
Today I'm gonna go over some head-scratching (some of which is still bugging me) I did on the weekend. I kinda got stuck with how I should be testing something, and this raised questions as to whether I even should be testing something. It's a reasonable exercise to go through, so I will document it here. First: recap.

This is an ongoing series of articles about unit testing and TDD (and will diversify from there as I progress), with - so far - five previous entries:

And to remind you where the code is at, here's where it currently stands:

This is the function we're testing (there's not much to it yet!):

// function.cfm
function createEventObject(){};

And here are our tests so far:

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

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

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

    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 also have a custom assertion (for the hell of it, more than necessity):

// 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 this results in this test run:



Remember we write the test for checking what got returned from the function, but we didn't update the function (I probably should have included that in the last article, sorry). So let's get that test to pass:

// function.cfm
function createEventObject(){
    var eventContainer = {};
    return {
        on = function(){},
        trigger = function(){}
    };
};

Results:




It's worth mentioning again that whilst it's taken me three blog articles to get to this point, so it might seem like a mountain of work, in "real time" all this stuff took about 10min maybe. Basically I sat down to decide how the function will need to work - just at a mechanical level so far - and wrote a test for each step of that, and then back-filled the logic to make the tests pass.

I had my first dilemma at this point. I'm not used to testing functions which return functions, so I'm not sure to what degree to test the returns from this function. Normally if I was returning a simple value, I'd just confirm my expectations, like the multiply() test I showed you yesterday:

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

And with more complex objects such as a struct, I'd test the structure of it, similar to how I test for the on and trigger keys in the test above. And if the struct was complex, I test all that complexity (and wish I was returning an object, not a struct!). But I would not test the contents of the keys, as that's generally data.

I also try to avoid "testing CFML". If I have a function which - in its definition - says an argument is a struct, I do not bother to test that it won't accept a string instead. This is not testing my own logic, this is testing whether CFML works. And I'd like to think the Adobe / Railo guys have already done that for me. And if they haven't, it's not my job to do anyhow.

However in this case, I kinda feel like I should be testing the "integrity" of what I'm returning from the function: making sure the returned on() and trigger() functions return functions of the correct signature. This would be dead easy if CFML supported delegates, but it doesn't so that's no help.

Aside:

I've written about delegates in a coupla articles, and raised an enhancement request:
Just quickly, if CFML had delegates, then I could just do this:

assertIsTypeOf(
    eventObject.trigger,
    "TriggerDelegate",
    "The returned trigger() function needs to be a delegate of type TriggerDelegate"
);

Where TriggerDelegate (NB, to be clear: that's the noun not the verb. In my NZ accent I'd be saying "deleghit" not "delegayte") could be defined as:

delegate void TriggerDelegate(required string event, optional struct additonalParameters);

Reminder: a delegate is a "type" of function. Kinda like to a function what an interface is to a CFC.

But anyway...

So - rightly or wrongly - I've decided to check the method signature of the returned functions too. Before I can do this, I need to work out what the method signatures should be.

on() will bind an event handler to an event, so it'll need at least those two arguments: event and handler. It might also need some data (as inspired by JQuery, to an extent: on()), at some point, but that's "at some point" so out of scope just now. This is TDD which is iterative, and I should not be implementing anything "just in case".

trigger() will fire an event. so all it needs to do is to take which event to fire. Similar to above it might need to pass some additional parameters at some point (as per JQuery's trigger()), but we'll deal with that later.

// Test.cfc
// NB: only new code is listed below

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

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
        );
    }
}

Note that I've refactored slightly here, I've added a setup() method which creates variables.eventObject, as I need it in every test. Remember that setup() gets called before every individual test function.

Basically in these tests I simply call the returned methods passing in values for all arguments except the one I'm testing the requiredness of. So when I'm testing that on() requires an event argument, I don't pass that in, but I do pass in anything else that I need to make the function work. This is so I got get other problems with the test, and I am only testing the one thing I intend to.

And the test here is that we're expecting it to fail at the moment. Remember the current state of the definition of on() is simply that it's a function:

// function.cfm
function createEventObject(){
    var eventContainer = {};
    return {
        on = function(){},
        trigger = function(){}
    };
};

So the test fails because we're expecting an error message complaining about the event parameter missing, and we didn't get it:





NB, my assertion test would normally be better than this fairly uncontrolled statement:

assertTrue(
    findNoCase("event", e.message) && findNoCase("parameter", e.message),
    failMsg
);

But that's the best I could quickly come up with that will work on both ColdFusion and Railo. Being realistic, it's unlikely any exception which matches that test will occur other than the one we're looking for.

Now we write the code to make those tests pass:

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

Another aside. If both Railo and ColdFusion returned the same exception if a required argument was missing - say a RequiredArgumentMissingException, then MXUnit can cater for that specific sort of thing:

/**
@mxunit:expectedException  RequiredArgumentMissingException
*/
public void function testRequiredArgumentMissingException() {
    throw(type="RequiredArgumentMissingException", message="A RequiredArgumentMissingException was raised", detail="This should have passed its test");
}

The mxunit:expectedException annotation means that if the test finishes with the specified exception, then that's a "pass" condition. If it doesn't error with that, it's a fail:

/**
@mxunit:expectedexception RequiredArgumentMissingException
*/
public void function testRequiredArgumentMissingException() {
    // no exception being raised
}



So that's quite cool.

I was going to continue onto my next dilemma, but I'm short on time today, so in the spirit of quick iterations, I'll get this out the door and discuss this next quandary (as yet not quite resolved), tomorrow.

Righto.

--
Adam