Saturday 2 November 2013

Unit Testing / TDD - MXUnit and test scenario

G'day:
OK, so I've prattled on a coupla times about TDD and unit testing from a theoretical point of view:

I think today I'll actually get around to writing some code.

This assignment I'm setting myself could all end in tears, as I'm really putting my money where my mouth is... not only am I going to take a straight TDD approach to writing these tests, I'm also actually doing the design of the function I'm testing as I go too. I have just an idea of how something might be able to work, but I don't actually know if it will be viable yet: I've gone over it in my head, but I've not written any code yet. Other than the test rig.

Similar to my code deferral function from a coupla weeks ago ("Threads, callbacks, closure and a pub"), which handles calling blocks of code in an asnyc fashion, running callbacks at various milestones in execution, I started to wonder if I could write an event-driven system in a single function too. Only a proof-of-concept sort of thing, and most as an exercise in leveraging closure in a real world way (most closure examples I've seen don't actually leverage closure, so I'm all for examples that do), and just to see if the thought exercise can pan out into real code.

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.

Initially I was just gonna bang this function out and see if it works in a useful way, and if so post it to at least this blog and perhaps CFLib. And I have to admit for my "home" work I don't generally do TDD. I probably should, but... hey. I hasten to add that any code that will go into production, I do do TDD. Always.

Anyway, I'm beginning to talk too much [ed: there - again - ends up being far too much talk and not enough code in this. Sorry]. Code.

First things first; MXUnit. This is not gonna be a tutorial in using MXUnit, but I'll note down what I have done to get moving with it.
  1. Get MXUnit from the MXUnit site (as per link above);
  2. Install it:
    • unzip
    • put it in a dir browseable as /mxunit
    • err...
    • ... that's it
  3. Test it by browsing to http://yourDomainHere/mxunit/. You should see this:

If that doesn't work, seek help on the MXUnit mailing list on Google. You can do stuff like integrate MXUnit into Eclipse / Sublime Text and that sort of thing. For the purposes of this series, I'm just gonna set it up to run in the browser, as it makes for easier/clearer screen-capturing.

And from there... we're off. First step: we need a test CFC. If we were planning an entire app, we'd need to com up with a file system strategy here. Generally speaking one doesn't want one's testing code mixed in with application code, and in the normal sequence of events if I had an application file structure that was /me/adamcameron/someApp, I'd have a parallel file structure /_test/me/adamcameron/someApp, and if I had /me/adamcameron/someApp/aSubDir/and/A.cfc, I'd have /_test/me/adamcameron/someApp/aSubDir/and/TestA.cfc. However in this case I've got a single UDF I'm testing, so I'm not messing around like that (and it wouldn't really work anyhow). So I have this file structure instead:

function.cfm - contains my UDF that I'm testing. Well at this stage it's empty, but it will contain my function
Test.cfc - contains my tests
runTests.cfm - contains some MXUnit code to run the tests in a given subdirectory in a browser

I'll get the MXUnit infrastructure side of things out of the way first, then we can mostly ignore it from there.

<!--- runTests.cfm --->
<cfoutput>
#new mxunit.runner.DirectoryTestSuite().run(
     directory = getDirectoryFromPath(getCurrentTemplatePath())
).getResultsOutput("html")#
</cfoutput>

This is the code that runs the output that we saw before. There's a bunch of various config and output options, but I'll leave that for you to investigate. I usually put a variation of this code in each subdirectory of my _test dir, so I can run any subset of tests at any given point in time. In this case I've only got the one test CFC, so that's not relevant.

Next I have my test CFC:

component extends="mxunit.framework.TestCase" {

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

     public void function setup(){
     }

     public void function tearDown(){
     }

     public void function afterTests(){
     }

     public void function baseline(){
          fail("not implemented");
     }

     private void function testHelper(){
     }

}

There's a few bits 'n' pieces in here worth touching on.

MXUnit infrastructure

The MXUnit-specific stuff is highlighted. All test CFCs must extend mxunit.framework.TestCase. This avails all the assertions we'll be using, plus has default implementations of the following methods that a test CFC might have.

Before I start: a bit about how the tests in a test CFC are executed. That run() method in runTests.cfm will load all test CFCs in a directory, then one by one run all the tests in each CFC in turn. For a given test run there is no guarantee what order tests are run in, although in reality it's down to how ColdFusion exposes them in a CFC's metadata, I think. Their execution order is certainly not randomised. But one should not assume any test execution order. And, indeed, each test really needs to be completely discrete from other tests in the CFC. But there is some prescribed behaviour one can leverage to control the test environment.

beforeTests()

Perhaps unsurprisingly from its name, this method - if present - is run once before any of the test functions are run. To this end, put any config settings a given CFC might need. I'd say that if you had any more than a inconsequential amount of code in here, it points to the code you're testing being poorly-written, as it suggests your code under test has a lot of external dependencies. However if your test CFC has dependencies needing to be injected, then here's a place to initialise them. Bear in mind that if this code errors, then testing halts with a ColdFusion error (well it did in the last version of MXUnit I looked at). I consider this a bug and have raised it as such, and at one point I even fixed it, but I've since lost the code. Before submitting it back to the MXUnit bods. I should revisit this.

setup()

This function - if present - is called before each individual test. So if your CFC has ten tests in it, beforeTests() gets called once, setup() gets called ten times. I often create the instance of the CFC I'm testing in here, if it's a stateful one. This means each test gets a new object to test on. If the CFC I'm testing is not stateful, it's OK to create it in beforeTests(). And this'll save some clock cycles too. There's an argument to be had either way as to whether a test should test test on a fresh object or it should be OK to run all tests on a single re-used object. I'm unsure of the best answer to this, and use a mishmash of using beforeTests() or setup(). If the code in this function errors, just the one test will be reported as a error, but otherwise the test run will continue.

teardown()

This optional function is run after each test. If there's some clean-up needed after a test run: eg deleting files that might get created, or somesuch: put it in here.

afterTests()

This optional one is the equivalent of beforeTests(), except runs after the tests run. Sometimes clean-up code is better to be run after all tests are run, rather than after each test.

Any other method in a test CFC is considered a test, if its access is public, or ignored if private. Private methods can be used as helper methods if there's a need for them. Again, if one needs a lot of helper methods: one's code is probably over- or under- engineered, perhaps. This is not always the case, but it's a reasonable orange (as opposed to red) flag.

Tests

So in our Test.cfc, we include function.cfm in beforeTests(). As we're only testing a single function, this is fine.

And our other method - baseline() - is our first test. And, true to TDD it fails. Indeed I've specifically told it to fail(). Any new modification to a test should have it fail. So we're off with our TDD here. We have a baseline() test, and it fails. Get used to the thinking that "failure" is not automatically a bad thing in gthe context of TDD: it's just part of the process.

Remember that the central tenet of TDD is in its name: Test-Driven Development. This means we design our code as follows:


  1. decide / design some functionality. Remember that the design phase of programming does not involve code; it involves planning and thinking. No typing. Certainly no typing in a CFML file (you can type notes if you like, but not code! That said, if you need notes... you're probably planning too much for this iteration). And we're not designing all the functionality at this point, just the next piece of it.
  2. Write a test for that functionality. You know what your functionality ought to do, so you can infer a test (or more than one test, if there's logic branching going on... more about that later) for it. If you know what it's supposed to do, you can write a test to make sure it does that. If you can't write a test to test the requirement, your requirement is not well-enough defined to continue with yet. Go back to 1. Equally: if you reckon you can't write a test for it... how do you know if it works? If it's not testable: why are you writing it? The code has to be doing something, and you need to know what it's supposed to be doing. So it's testable. Or yer just wasting time.
  3. Run the test. It will fail. It should fail. If it doesn't fail, you're not testing properly, because you're clearly not correctly interacting with the code that will implement the functionality. Because if you were interacting with it, you'd get an error because you haven't written that code yet. Remember that in TDD the test comes before the code being tested.
  4. That said, if you get errors in areas other than in the area you're about to code... you've just discovered a bug in your code, so... got back to step 1 and deal with the bug before progressing.
  5.  Step 4 notwithstanding, you now write code to make your test pass. And that's all the code you should write. Just enough to make the test pass. If you write code that - if you took it back out the test would still pass - you've written too much code. Put it to one side for later. The aim here is to make the test pass. That's it. Small steps.
  6. That said, in the course of writing the code you might go "oh, hang on... I need to do this as well/instead". If you do... go back to 1 and firm-up the requirement, and repeat the cycle.

That looks like a lot of work, but realistically it's "I'll make it do this... work out how to make sure it does it... make it do it... fix yer mistakes... move on to the next thing". This is exactly what you do when you code anyhow. Except you'll kid yourself into thinking you can test it in your head, and you probably won't spend sufficient time planning stuff. This is the chief difference in TDD: you're forced to plan what you're doing, any you're forced to test your plan works. And by the nature of it, one needs to break out a monolithic way of thinking/planning/coding, and think in more discrete and manageable units. It's just a "win". And surely you can see it's a better approach than to just write random untested code as you go?

If you read the comments on the preceding articles in this series you'll see some very typical - actually to the point of cliché - push back on this whole notion: there's no time, there's no money, the code's all too complicated, etc. That's all bullshit. Bullshit. We're doing exactly this process whenever we write code, the difference is we leave it too late to apply the checks and balances to our work, by which time we don't know what's causing problems and where things are going and there's so many moving parts we have to basically re-design the code again to work out why things might be going wrong.

So... anyway... back to the code. We're in fine stead: we have a failing test. However it's a fairly artificial situation as we're actually using the fail() method to cause the failure. We're not even calling our function yet. We don't have a function yet.

The first step here - and I'm belabouring the point slightly by design - is to create the function. Once we have a function, we can at least call it, and that's a passed "test". I am making a thing out of this as initially I was gonna say don't worry about the name of the function to begin with. If you don't already know what the function ought to be called (and I don't know the name of this function currently), just call it f() and move on. Or foo(). Or nameToBeDecided() or something. I've actually done this in the past when we can't quite come up with the name of a function, and people are offering different variations of similar very technical / jargon-oriented names, for some mundane functionality. However I think this is actually poor practice. If you're writing a function, you ought to already know what it's going to do, and the name of the function should describe what it does. And it should describe all of what it does. This is important because functions should be simple, and do one thing, and do it well. So part of the planning process is to work out what your function is doing, and - as a sanity-check - give it a name that is appropriate. And if you haven't got a name in mind, then you're not ready to start yet. So don't use temporary names. Work it out ahead of time.

Aside:

This is nothing to do with TDD. If you come up with a function name serveDinner() (it's getting on to that time of day, sorry), then all it should do is put the food on the plates and bring it out to the table (or bring the food to the table so people can serve themselves. Whatever). It should not involve cooking the meal. Because it's - at best - misnamed, as it should be prepareAndServeDinner(), but - given there's an "and" in there - the function is probably doing too much. There should be separate prepareMeal() and serveMeal() methods. There might be an overarching haveDinner() method which internally calls prepareDinner() and serveDinner() (and eatDinner()!).

This is overstated, but it demonstrates that there's significance in one's method names, and also I think from the POV of TDD, it's important to know what one is setting out to do before doing it. Because eating dinner and preparing dinner is a different thing.

Aside2:

Tonight's dinner is going to be pizza. Eaten by me, prepared by one of the spotty youths at the local pizza shop.

So I need to think of a function name before I continue.

On one hand, people are very het up about functions needing to be formed along the lines of verbNoun(). Because methods do stuff, and the vague glimmer of English they recall from school is that "verbs" are "doing words". And at some stage someone wrote some words down and said "methods should be verbNoun()". And He saw that, and it was good (to either quote or paraphrase Genesis. The book, not the band). Sorry: I'm calling "bullshit" on that. For one thing... look at the Java API: toString()? indexOf()? matcher()? Perfectly understandable method names, without being so literal as to have to describe the action they perform using a verb and a noun. I think this verbNoun thing is a completely specious assertion. For another thing: we're not planks, so sometimes the "doing" bit can be inferred from the context. I think some people prefer following rules than they prefer using their brains. That said, I think verbNoun() is a good guideline, but let's not get too dogmatic about these rules. They're not rules. They're guidelines.

The reason I mention this is because I'm struggling with a name for my function! I tried to go verbNoun() with this, but none of the options I considered sit well with me: createEvents() (no: it's not creating events: it's creating some methods, not the events); createEventMethods(): it's not really describing the intent of the code. And - on reflection - they're not methods, as there's no class/object involved. So... erm... createEventHandlers()? No, because whilst these are functions that handle events... an "event handler" is a pre-defined concept in programming, and indeed within the precise area this code is for, and in that context: what this function returns are not event handlers. So. Argh. createEventObject() it is. This sounds very generic and wishy-washy, but the last para took about an hour to write (and close to two pints of Guinness) whilst I vascillated over all this, so it'll do for a start. The good thing about TDD (and an environment with good unit test coverage) is that refactoring later on is easy. And safe.

So there's our first iteration of TDD for this done. My baseline test becomes:

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

And I run that, and I get this:



And if I then fulfill step 5 from above: I write the code to make the test pass:

<cfscript>
     // function.cfm
     function createEventObject(){};
</cfscript>

And rerun:

Done.

Our first iteration of our first unit test passes. TDD in action. Now it took a while to read this (and about 6h to write it, and another hour to actually publish it), but in the normal scheme of things this describes about 10min work. And is the first small step in a series of small steps to write better, test-driven code.

Which is a good thing.

I've been at the pub for the last 4h (and 5 pints), and I am offline (using EverNote to write this). I need to go back to the B&B to use internet to publish this, so I will do this now... and quickly... because it's 18:30 and the Munster v Ospreys game is on in an hour, which I want to watch. The Ospreys were on my flight last night and I chatted to one of their wingers, so I'm supporting the Ospreys tonight. Note to non-rugby watchers: Munster is the home team, and an Irish one. The Ospreys are from Wales. I'm sitting in an Irish pub. I will be the enemy :-). But I better crack on with it.

--
Adam

PS: [time passes] About to press "publish", but I've now mised the first quarter of the game :-(