Tuesday, September 21, 2010

Unit Testing Custom Tags with MXUnit

Introduction

We love MXUnit at work and at Mach-II to unit test our components and to do integration testing.  In Mach-II Simplicity (1.8), a big feature was our form and view custom tag libraries.  These features made developing the UI (view) layer of your application easier and less prone to error.  Why would you write this kind of form with manual data binding:


<form method="post" encType="multipart/form-data" action="#BuildUrl('processUser')#">
        <h4>First Name</h4>
        <p><input id="firstName" value="#event.getArg('user').getFirstName()#" type="text" name="firstName"/></p>
        <h4>Last Name</h4>
        <p><input id="lastName" value="#event.getArg('user').getLastName()#" type="text" name="lastName"/></p>
        <p><input type="submit" value="Save Changes" /></p>
</form>

When you could just write this and have auto binding:


<cfimport prefix="form" taglib="/MachII/customtags/form" />
<form:form actionEvent="processUser" bind="user">
        <h4>First Name</h4>
        <p><form:input path="firstName" /></p>
        <h4>Last Name</h4>
        <p><form:input path="lastName" /></p>
        <p><form:button value="Save Changes" /></p>
</form:form>

Our First Pass Testing Solution and Failures

What this brings up is that a lot of complex logic is now inside of custom tags*.  How do you test custom tags?  For the most part, a lot of the logic in custom tags should be about output and not data manipulation.  MXUnit makes it easy to compare data if the results are simple values, structs, arrays, queries, etc.  However, the output from a custom tag is not just a simple string like "abc123" but more a complex string with nesting of tags and relationships between tags (for example a "select" and "option").

When we first started down this path of testing our custom tag libraries, we started by doing straight up text comparisons:


<form:form actionEvent="something" bind="${event.user}">
            <cfsavecontent variable="output"><form:input path="firstName" /></cfsavecontent>
</form:form>
<cfset assertTrue(output EQ '<input id="firstName" value="#event.getArg('user').getFirstName()#" type="text" name="firstName"/>') />

This was simple but lead to an easy break down down the line.  There were two sticking points:

1) Our custom tag library has a "tag writer" relies a lot on looping over a struct of tag attributes .  As we all know in CFML, we cannot rely on the order of iteration of a struct.  So while assertion works on OpendBD it sometime failed on ACF8 because we were relying on the custom tag to ouput the tag attributes in the same order as our assertion string.  In all reality, the browser does not care about the order of the tag attributes and neither should our unit test.  The flexibility of the unit test was strongly coupled to string as a whole; not the individual parts.

2) It is hard to test relationships such as <select> and <option> tags in straigh string comparison.

2) Maintaining the unit tests becomes painful because of the first and second point.

These limitations were all deal breaker after just a week of trying to get tests to run all the time.  So on to find another solution that is less coupled to the straight string comparisons.

Our Second Pass Testing Solution and Failures

Ok, let's test individual parts like the value of an attribute using RegEx or Find() built-in functions:


<form:form actionEvent="something" bind="${event.user}">
            <cfsavecontent variable="output"><form:input path="firstName" /></cfsavecontent>
</form:form>
<cfset assertTrue(Find("value="farrell"', output)) />
<!--- More assertions --->

This works, but it is still extremely clumsy having to check for each attribute and we still cannot check the parent child relationships of tags like <select> and <option>.

Our Third Pass Testing Solution and Ultimate Successful Approach

How do you solve this predicament? We "sort of" solved problem #1, but #2 and #3 are still unsolved in my book.  Let's think about what the custom tag is outputting.  Hmm...our custom tag library output XHTML compliant code and there are many tools available to use to leverage XML .  So for all of you who think XML is bad, wrong and decitful -- it's time to eat your words. Luckily for us, MXUnit has a built-in assertion called assertXPath() that fits this use case perfect.

I know you're tired of me jabbering about how we got here so you'll want to see some code right?  Bare in mind that the Mach-II custom tags do some interaction with what is happening in a Mach-II request so we had fun setting up a fake request so the object required by the tag are in memory.  For the sake of brevity, I'm going to leave that setup code out of my examples however you can always see them in the Mach-II SVN repository (the joys of open source) if you want to learn more.

Let's look at testing a simple text input:


<cffunction name="testInput" access="public" returntype="void" output="false"
    hint="Test basic 'input' tag.">

    <cfset var output = "" />
    <cfset var xml = "" />
    <cfset var node = "" />
    <cfset var bean = CreateObject("component", "MachII.tests.dummy.User").init() />
    <cfset var event = variables.appManager.getRequestManager().getRequestHandler().getEventContext().getCurrentEvent() />

    <!--- Add data to the the bean and set to the event so we can do binding --->
    <cfset bean.setFavoriteColor("red") />
    <cfset bean.setLastName("Farrell") />
    <cfset event.setArg("user", bean) />

    <cfsavecontent variable="output">
        <root>
            <form:form actionEvent="something" bind="${event.user}">
                <form:input path="favoriteColor" />
                <form:input path="lastName" />
            </form:form>
        </root>
    </cfsavecontent>

    <cfset xml = XmlParse(output) />
    <cfset debug(output) />

    <cfset node = assertXPath('/root/form/input[@type="text" and @value="red" and @id="favoriteColor"]', xml) />
    <cfset node = assertXPath('/root/form/input[@type="text" and @value="Farrell" and @id="lastName"]', xml) />
</cffunction>

All XML documents require a "root" node of some sort.  In HTML, it's the <html> tag so in our unit test we just used <root> so XMLParse() wouldn't choke on the code.  We also want to check that the "id" attribute and the "value" attributes are what is expected and we can easily do that in our XPath assertion string.

Ok, so that's a simple example.  What about something more complex?  Let's look at how we test our <form:radiogroup> tag.  This tag takes a collection (lists, struct, array, array of structs or queries) and turns them into a group of radio buttons based off a template:


<cffunction name="testRadiogroupWithQueries" access="public" returntype="void" output="false"
    hint="Test basic 'radiogroup' tag.">

    <cfset var output = "" />
    <cfset var xml = "" />
    <cfset var node = "" />
    <cfset var bean = CreateObject("component", "MachII.tests.dummy.User").init() />
    <cfset var event = variables.appManager.getRequestManager().getRequestHandler().getEventContext().getCurrentEvent() />
    <cfset var colors = QueryNew("v,l") />

    <!--- Add data to the the bean and set to the event so we can do binding --->
    <cfset bean.setFavoriteColor("red") />
    <cfset event.setArg("user", bean) />

    <!--- Test with simple array --->
    <cfset QueryAddRow(colors) />
    <cfset QuerySetCell(colors, "v", "red") />
    <cfset QuerySetCell(colors, "l", "Big Red") />
    <cfset QueryAddRow(colors) />
    <cfset QuerySetCell(colors, "v", "green") />
    <cfset QuerySetCell(colors, "l", "Giant Green") />
    <cfset QueryAddRow(colors) />
    <cfset QuerySetCell(colors, "v", "brown") />
    <cfset QuerySetCell(colors, "l", "Bad Brown") />

    <cfsavecontent variable="output">
        <root>
            <form:form actionEvent="something" bind="${event.user}">
                <form:radiogroup path="favoriteColor" items="#colors#" labelCol="l" valueCol="v">
                    <label for="${output.id}">${output.radio} <span>${output.label}</span></label>
                </form:radiogroup>
            </form:form>
        </root>
    </cfsavecontent>

    <cfset xml = XmlParse(output) />
    <cfset debug(node) />
    <cfset debug(output) />

    <cfset node = assertXPath('/root/form/label/input[@type="radio" and @value="red" and @id="favoriteColor_red" and @checked="checked"]', xml) />
    <cfset node = assertXPath('/root/form/label/input[@type="radio" and @value="green" and @id="favoriteColor_green"]', xml) />
    <cfset node = assertXPath('/root/form/label/input[@type="radio" and @value="brown" and @id="favoriteColor_brown"]', xml) />
    <cfset node = assertXPath('/root/form/label[@for="favoriteColor_red"]/span', xml, "Big Red") />
    <cfset node = assertXPath('/root/form/label[@for="favoriteColor_green"]/span', xml, "Giant Green") />
    <cfset node = assertXPath('/root/form/label[@for="favoriteColor_brown"]/span', xml, "Bad Brown") />
</cffunction>

As you can see, we setup some test data and pass that to the "items" attribute of the <form:radiogroup> custom tag.  The tag doesn't know which columns to use for the value or for the label so we indicate that.  Let's take a closer look at the template that is being used:


<label for="${output.id}"><span>${output.label}</span> ${output.radio}</label>

This template is interated over (i.e. looped over) for each item in the query.  We use a simple placeholder syntax of ${} to indicate where the radio and label are outputted (other computed attributes like the "id" are also available).  Our assertations in this case not only test for tags heirarchy and tag attributes, but the inner text of tags.  In this case, we need to test that the value of "${output.label}" that is displayed is correct (not the actual <label> tag).  We wrapped a <span> around it so we can easily find it with our XPath assertion.  Also, when testing for the "${output.label}" we need to make sure we're grabbing the right one so the XPath searches that the <span> is inside of a <label> with the correct "for" attribute.

In Closing

Testing custom tags with MXUnit is easy if you figure out the right plan of attack and after three iterations to our testing strategy -- we found one that works.  I'd be happy to hear if people have suggestions on improving how custom tag output can be tested with MXUnit, however at this point I think this solution is rather slick and leverages XML/XPath without the need for any custom assertions to be written in MXUnit.

Now no code should go un-tested including custom tags.  This is especially true in something like Mach-II which is a community asset and having unit tests makes the software a better product.  So now go forth and unit test your custom tag libraries!

* Sidebar: For performance, we used straight up custom tags with a "UDF"-like function library instead of having custom tags call CFCs.  Our basic testing at the time showed that CFCs were 10-15 times slower than the custom tag with "UDF"-like function library (included via a cfinclude).

Thursday, September 16, 2010

YAF - Yet Another Framework: Giftware Versus Open Source

With the seasons changing and Fall fast approaching, it must be time for yet another framework (YAF) to show signs in the CFML world.  I'll give credit for this post to a tweet from Matthew Reinbold:

Playing with new ColdFusion framework that still is in stealth mode. Do programmers need another MVC/ORM set of scaffolding? Let's find out.

Before I take issue with having another framework pop up in the CFML world and why I feel that it is possibly detrimental to our small sect of programmers, I want take issue -- moreover -- more notice to the word "stealth" in the tweet.  What bothers me there is a big difference between developing a framework for internal / personal use versus public consumption.  If I was developing an internal "framework" for my personal use, having input from possible users of my "framework" really would not matter.  I would be using it for my own needs; Programmer Paul doesn't need to put his own two cents on how feature Z should work.  On the flip side, developing a framework for public consumption in a vacuum (i.e. "stealth" mode) is less than beneficial to all parties that possibly would use the framework.  Therefore if Programmer Paul had interest in what I was doing, his knowledge can only make the "framework" better.  This means that public interaction, even if it's just one person, is beneficial.  You can't pay somebody enough to give you bold and honest views on stuff.  This begs the question: How do you make somebody care?

Developing transparently is key to building an user base.  Without transparency in a project, you cannot call yourself an open-source project.  I've found out in that most developers consider the main tenant of open source is that the code is freely available (i.e. the "license").  Giving users the software for free where the software is developed in a vacuum and there is limited means or encouragement to contribute back to the project is merely giftware*. Open source is much more than the license; it's about philosophy on how software should be developed and a community that rallies behind it.

The big different, other than transparency in developing software in the open, is that interested parties will help you develop new features if you let them be "shareholders" of the project.  As with anything in life, nobody cannot care about everything.  Humans weren't designed to function that way.  We make hundreds of decisions everyday on what we spend our time on, what is important to us and what we do so we are happy individuals.  This is no different in open source software.  Consider this situation:

If Developer Dave uses Project Perfect and finds a Big Bug, what encourages Developer Dave to contribute his fix back to the Perfect Project instead of just working the next Alluring Application that will make millions when it hits the world?

The answer to this is: nothing.  Unless you are financially tied to open source to make a living, most developers don't count themselves shareholders in the project because they don't own it.  Whereas if the project  helps you succeed at your job, there is a reason to become involved in some of these projects.  This is where being transparent, making people's opinions count and accepting (acceptable / valid) contributions into a project makes everybody in your user base a potential shareholder and champion of your project.  Making this conversion of your user base to shareholders is something that needs to be cultivated from the start of the project.  It's nearly impossible to make conversions when your project starts in "stealth" mode because it sets the "tone" of the project from the start.  Transparency is the biggest reason in my book why 99% of all CFML "open source" projects are just giftware and will always be one-man operations.  Giftware is just viewed by "Developer Dave" as a free tool where open source is more -- it's a community of like minded folks using the tool.  All in all, the side effect is a better software.

Therefore if you make philosophy and transparency #1 on your list, will you see your project succeed (provided it's a good tool too).  Otherwise, you'll just be YAF that gets forgotten in 6 months time.

* I want to attribute the term "giftware" to by good friend Matt Woodward.  I heard it applied by him first so I can't take credit on the term.

Tuesday, September 7, 2010

Last call for BFusion/BFlex 2010 Registration - Best & Cheapest Conference Around - 9/11/-9/12 Bloomington IN

BFusion/BFlex 2010
Hands-on training for developers and designers from industry leaders coming to IU. You will not find a more effective and cost-effective professional development opportunity.
 
Want to learn more about ColdFusion and Adobe Flex? What about cross-platform AIR applications? Already handy with Illustrator and Photoshop and want to quickly turn your work into Rich Internet Applications? Whether you are a curious beginner or an advanced developer, BFusion (a full day of ColdFusion training) and BFlex (a full day of Flash Platform development) have something for you:



  • What: Hands-on training from the experts in ColdFusion, Flex, AIR, Catalyst, and other technologies

  • When: September 11 and 12 (Saturday and Sunday)

  • Where: IU Bloomington

  • Cost: $30 for one day, or $45 for both days (price includes lunch)



Designers: BFlex also includes an all-day session for you (no programming necessary), Flash Catalyst - From Design to Rich Internet Applications without Coding.

For more information about the sessions, speakers, and registration,  please see:  http://bflex.indiana.edu

See this post from one of our speakers Matt Woodward, Principle It Specialist of the US Senate, about his take on this year’s event: http://blog.mattwoodward.com/get-your-hands-dirty-at-bfusionbflex.

The event is also listed on Facebook and LinkedIn. Please share it with friends and colleagues.

On Facebook: http://www.facebook.com/home.php#!/event.php?eid=108657742524082
On LinkedIn: http://events.linkedin.com/BFlex-BFusion-2010/pub/4046