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:
- Unit Testing / TDD - initial rhetoric
- Unit Testing / TDD - why you shouldn't bother
- Unit Testing / TDD - MXUnit and test scenario
- Unit Testing / TDD - why you should bother
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:
- I'm gonna write a function.
- I'll need tests (creates Test.cfc).
- Deliberation as to what to call the function.
- I'm gonna call the function "createEventObject()".
- Writes a test to call createEventObject().
- Writes function called createEventObject().
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).
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).
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:Simple! This is one thing to try to strive for in ones tests: keep 'em simple.
- 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
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