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).

2 comments:

  1. Peter, Can you shed some light on your MXunit setup in conjunction with MachII or point me toward a tutorial? The avalanche of framework objects that need to be loaded in order to run most of my tests makes it pretty challenging and time consuming to get started. This is especially true when using Coldspring. Moreover the multiple ways one's machII application might be setup makes helpers like stubbie useless without tons of mods.

    ReplyDelete
  2. Thank you for the great time on your blog. I often look for the post these wonderful items you share. Really interesting. Good luck to you!
    Tarot gratuit ligne , Medium serieux

    ReplyDelete