Today I'm resuming where I left off yesterday, but first the obligatory links to the rest of the series so far:
- 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
- Unit Testing / TDD - more tests, more development
- Unit Testing / TDD - getting stuck on how / what to test (part 1/2)
Firstly, the baseline code I am working with today is in this Gist: https://gist.github.com/adamcameroncoldfusion/7582867. I'll not repeat it all here, other than the function itself, which is currently:
function createEventObject(){
var eventContainer = {};
return {
on = function(required string event, required function handler){},
trigger = function(required string event){}
};
}
The next thing I want to do is to make the on() function actually deal with its arguments. What it needs to do is to put the values into some sort of data structure within the main createEventObject() function, which will be "shared" between on() and trigger() via closure. However what do I test here? If I was testing a CFC instance I could take this approach:
// C.cfc
component {
variables.myProperty = "";
public void function setMyProperty(required string myProperty){
variables.myProperty = arguments.myProperty;
}
public string function getMyProperty(required string myProperty){
return variables.myProperty;
}
}
// TestC.cfc
component extends="mxunit.framework.TestCase" {
public void function setup(){
variables.oTestC = new C();
variables.oTestC.getVariables = getVariables;
}
public void function testSetMyProperty(){
var valueToSet = "TEST_setMyProperty";
variables.oTestC.setMyProperty(valueToSet);
var valueActuallySet = variables.oTestC.getVariables().myProperty;
assertEquals(
valueToSet,
valueActuallySet,
"setMyProperty() set myProperty incorrectly"
);
}
private struct function getVariables(){
return variables;
}
}
Here we're testing setMyProperty(). What we want to test is that when we call setMyProperty() that the property is actually set. Now... strictly speaking we could employ the "what happens in Vegas stays in Vegas" sort of rule here: we don't actually care what happens internally in the CFC. As long as the API itself works. So provided setMyProperty() runs... job done. But I think that's slightly too dogmatic. We do actually want to know that setMyProperty() worked, and its definition of "worked" is that variables.myProperty gets set to the value passed into setMyProperty(). I think this is worthwhile testing. But mileage varies on this, and purists will be shaking their head.
Another thing here is that a very easy way to test that setMyProperty() works without worrying about the inner workings, is simply to call getMyProperty(), and see what it returns. If the value makes the round trip: we're good. This is where the purist in me kicks in. To use getMyProperty() to test setMyProperty()... we are kinda assuming that getMyProperty() works. And how would we be testing getMyProperty()? Well extending this logic, probably by using setMyProperty() to set something and then testing getMyProperty() pulls back that value... but that assumes that setMyProperty() works... which is all a bit circular. In reality I think that testing approach is fine, but I'm using another technique here, which only tests that setMyProperty() does indeed set variables.myProperty.
What I do is have a function getVariables() which returns variables, and I leverage one of the cooler features of CFML in that one can poke new functions into an object if one wants. When I call getVariables() having injected it into my object, and call it... it returns the variables from the context it was called in... which is the object's internal variables scope. Cool.
So I call setMyProperty(), then grab the object's variables scope by calling oTestC.getVariables(), and check that myProperty has the value I wanted to set it to. Job done.
If I was wanting to test getMyProperty(), I'd perform the same action in reverse: manually poke a value for myProperty into the object's variables scope, then call getMyProperty() and see if it was the same value. Done.
As a rule, I don't do this sort of variable value checking, except with accessors.
But... back in reality land... none of this helps us. We're not testing an object's methods, we're testing a struct which contains some functions, and a struct doesn't have a variables scope to poke and prod. What's more, the functions being returned are using closure to enclose a reference to a variable in our createEventObjects() function's local scope... which is completely inaccessible from the calling code. So we cannot use the above technique to test that on() is indeed adding an event listener to the eventContainer it has a reference to. I have racked and racked my brain as to how to test this without leveraging trigger() in the same way one my test a setter by calling its getter... but I drew a blank. So... you know what? I'm just gonna test on() and trigger() at the same time for a few tests I need to write.
OK... now that we've cleared that up... what are we testing? Well the first thing to test is that when we tell on() to add a handler to an event, that that's actually what it does. And we're gonna test that by adding a handler, and then triggering the event and making sure the handler is called. This is cheating, but unless one of you lot can suggest a better way to do it... this is is good as it gets.
So here's my test:
public void function testOnHandlerStored() {
structDelete(variables, "testOnHandlerStoredResponse"); // ensure it doesn't exist
var handlerResponse = createUuid();
variables.eventObject.on("TestOnHandlerStoredRun", function(){
variables.testOnHandlerStoredResponse = handlerResponse;
});
variables.eventObject.trigger("TestOnHandlerStoredRun");
assertTrue(
structKeyExists(variables, "testOnHandlerStoredResponse"),
"testOnHandlerStoredResponse should have been created"
);
assertEquals(
handlerResponse,
variables.testOnHandlerStoredResponse,
"testOnHandlerStoredResponse set incorrectly"
);
}
What we're doing here is:
- add an event handler which sets a variable in the variables scope;
- trigger the event
- check to see if the variable actually has been set.
In the current state of on() and trigger() the test fails, which is good. We have a correct test bed in place. Now for the code to make the test pass:
function createEventObject(){
var eventContainer = {};
return {
on = function(required string event, required function handler){
eventContainer = arguments;
},
trigger = function(required string event){
eventContainer.handler();
}
};
}
Bear in mind that this is clearly incomplete code - it's very fragile and easily defeated - but that's the whole thing with TDD: we work in small, iterative improvements. The thing being each iteration proves that it works, as well as not causing any regressions.
Continuing the spirit of iterative work... I'll break here. I have somewhere to be, and this is a reasonable chunk of reading. The good news is I've got all the code written for the next article, so that should come through shortly (measured in days, not hours ;-). See ya soon...
--
Adam