- It's a lot easier to test arrays for the data they contain than it is to test the database after the fact
- Since there's only one chunk of code that performs a database Update, I can easily "turn the update off", thereby not updating the database at all.
As I said, in my test, I want to do two things: Test the array for its data, and ensure the database update doesn't occur. This method, as originally written, almost gets me there. To the original programmer (my friend Mike), nice job! The obvious problem here, with respect to testing, is that the update will still happen. So what to do? Since the original code has a single block for the DB update, it's easy for me to pull out that chunk of code into a separate method, and then inside my test, override that method with one that simply doesn't perform any updates. I call this "Extract and Override". Here's my new code. It now contains 2 methods. I've simply extracted out that last chunk of DB updates into a separate method, and then I call that in the original function:
<cffunction name="moveSection" returntype="array" hint="resorts stuff" access="public"> <cfargument name="blah"> <cfset var sections = ArrayNew(1)> <cfset var ii = 1> <cfset var jj = 1> <cfset var kk = 1> <cfset var keyList = ""> <cfset var updateTemplateSections = ""> <!--- assload of logic to build up an array ---> ..... <!--- process array and extract sectionIDs in order ---> <cfloop from="1" to="#arrayLen(sections)#" index="ii"> <cfset keyList = listAppend(keyList,sections[ii].sectionID)> <cfloop from="1" to="#arrayLen(sections[ii].children)#" index="jj"> <cfset keyList = listAppend(keyList,sections[ii].children[jj].sectionID) /> </cfloop> </cfloop> <!--- finally, do the update ---> <cfloop from="1" to="#listLen(keyList)#" index="kk"> <cfquery name="updateTemplateSections" datasource="#getDsn()#"> UPDATE templateSections SET sortOrder = #kk# WHERE templateSectionId = #listGetAt(keyList,kk)# </cfquery> </cfloop> <cfreturn sections> </cffunction>
Finally, the unit test. Since I've extracted out the actual code that performs the update, it's trivial to override that with MXUnit in the test. All I do is create a new, private function inside the test itself that does nothing. Then, I use injectMethod to replace the existing function with my new for-testing-only function:
<cffunction name="moveSection" returntype="array" hint="resorts stuff" access="public"> <cfargument name="blah"> <cfset var sections = ArrayNew(1)> <cfset var ii = 1> <cfset var jj = 1> <cfset var keyList = ""> <!--- assload of logic to build up an array ---> ..... <!--- process array and extract sectionIDs in order ---> ..... <!--- call my new method, which I've extracted ---> <cfset performMoveSectionUpdate(keyList)> <cfreturn sections> </cffunction> <cffunction name="performMoveSectionUpdate" access="private"> <cfargument name="keyList"> <cfset var kk = 1> <cfset var updateTemplateSections = ""> <!--- finally, do the update ---> <cfloop from="1" to="#listLen(keyList)#" index="kk"> <cfquery name="updateTemplateSections" datasource="#getDsn()#"> UPDATE templateSections SET sortOrder = #kk# WHERE templateSectionId = #listGetAt(keyList,kk)# </cfquery> </cfloop> </cffunction>
<cffunction name="moveSectionDownResortsCorrectly"> <cfset injectMethod(gw, this, "performMoveSectionUpdate", "performMoveSectionUpdate")> <cfset sections = gw.moveSection(arg1,arg2,arg3)> <!--- perform assertions as needed ---> </cffunction> <cffunction name="performMoveSectionUpdate" access="private"> <cfreturn ""> </cffunction>
- in my original object, extract out the behavior that I don't want to occur, during my test, into a separate method
- in the test, override that method in my original object with a new, innocuous method
Carl Jung coined the term "synchronicity" to describe events that at first appear coincidental but which are potentially meaningfully connected.... a "meaningful coincidence". I've been thinking about this for some time now: that the advent of Rich Internet Applications (RIAs) could potentially be a tremendous boon for encouraging boring old server-side unit testing. Now, you might be thinking: "the two are completely unrelated". You can't get much more disparate. RIAs are sexy, they're hip, they're sleek and fun and whizbang. Unit testing, on the other hand, has got to be the least sexy coding you can do. It's the toothless, saggy-butt, scraggy-haired 4th cousin of cool technologies, at least to many people. Cranking out animated heatmap hoogies in flex is toot sweet. Writing "assertEquals(4,query.recordcount)" ain't.
The further removed you are from development-time access to debug information, the more time you spend debugging development-time problems.
Let that sink in. There is no cfdump/cfabort simplicity when your front end is a flash movie making remote calls to your backend CFCs. With richer interfaces, the time you spend debugging your backend code increases considerably. Not so sexy anymore, is it? But here's the thing: What if, rather than try to debug your code at runtime in the interface, you instead remove the interface from the equation. You stay in CF for your debugging. You get your CFDumps back. In short, you get closer to your data again... you get closer to the information you need to do your debugging. You're no longer so far removed.
This is where our ugly cousin, unit testing, comes in for the win. By testing your backend CF code with more CF code, you stay on the server and you catch your problems earlier. You codify the expectations of the code. You think more about the edge cases. You root out bugs earlier in the process and thereby make writing your RIA joyful again. You can focus your front-end efforts on the pizazz because you're not worrying so much that your queries are working as expected -- you've already worked out the server-side kinks in your tests. The common arguments against unit testing -- that it takes too much time, that you write too much code, etc -- start to fall away as it becomes apparent that the time savings you accrue start to overtake the lost time spent trying to debug server-side code from an RIA.
This is what I've been thinking about: that RIAs make it harder to debug server-side code. Therefore, they encourage more unit testing of server-side code because the value proposition of unit testing is clearer when put in contrast to the drudgery of runtime debugging the backend of an RIA.
Which leads to my moment of synchronicity today. I had planned on writing this blog post on how RIA development damn near requires backend TDD. Then, I came to work this morning, and in my feedreader was this: Indy Nagpal gave a presentation on server-side TDD at a recent RIA conference. Not at CFUnited or CFObjective or the local CFUG. At an RIA conference. I thought that was pretty cool, and it suggests to me that other (way smarter) people are thinking the same thing: that if you're writing rich internet apps, then server-side unit testing is getting harder to ignore.
- No duplicate entries. "com.my.SomeTest" will only ever appear once in the list, and will only reflect the last run. If you run that test 20 times, you won't get 20 entries in the list
- When running a directory of tests, you see the full path to the directory in the list
- It's easy to distinguish between single TestCase runs and Directory runs. Simply, directory runs start with a slash.
- A more reasonable start value for remembered test runs. JUnit defaults to 10 items in the history. Dunno why. MXUnit defaults to 30. Considering that duplicates never appear, this seems like a reasonable default. In addition, changing the default is simple and obvious.