Testing tabular output in Spock
On my current project, we’ve created a large suite of Fitnesse tests. These tests work quite well for validating certain types of data that are messy and difficult to visualize in normal unit tests. For example, if I want to test a function that returns a list of payments, we can write a simple table in the wiki to validate that entire list. We are not using most of the other features that Fitnesse was designed for however. The wiki format is nice, but firing up a server and a full application to test against is not really what we’re going for in most cases. Developers are the only ones who look at these tests so there is no reason for the wiki interface that Fitnesse provides. Forcing all of our setup code into fixtures is an unnecessary complication that adds extra maintenance.
Since our primary testing framework is Spock, and Spock has a great data table syntax, I thought it would be the perfect fit for these tests. I’d just move the tables over, convert some fixtures to helper functions and we’d have one less testing framework to maintain.
What I failed to think about initially is that Spock data tables are fundamentally different from Fitnesse query fixtures. While query fixtures are designed to output a list of data and validate the entire list, Spock data tables validate one row against an execution multiple times. What Spock does is what you want the vast majority of the time, but there are cases, like the one I mentioned above, where you want to validate a list of data returned by an execution. In these cases, a data table-like syntax is very nice to easily visualize what you’re validating.
Here are a few examples of how I approached this. Let’s say I have a function I’m testing that returns a list of dates and payment amounts. My expected output is a list of Payment objects. I want to validate the date and amount on each one of these objects. Using Spock without any data tables would look something like:
setup: //... when: def results = myClass.getPayments() then: results[0].date == '2013-01-01' results[0].amount == 100.00 results[1].date == '2013-02-01' results[1].amount == 150.00 //...etc
That works, but it’s kind of busy, and if you start adding other values to that object it gets messy very fast. What if I try to use data tables?
setup: //... when: def results = myClass.getPayments() then: results[idx].date == date results[idx].amount == amount where: idx | date | amount 0 | '2013-01-01' | 100.00 1 | '2013-02-01' | 150.00 //...etc
That certainly is a lot more readable. If I start to add other values to the object I just add some more columns and it will still be relatively easy to follow. There’s a huge problem here though. In this test I run the code under test for every iteration. That’s a lot of extra work just to get a nicer format for my test.
My solution to this issue was to create a simple Spock extension. You can find it at https://github.com/sjurgemeyer/spock-extensions. This extension provides a new annotation called SingleExecution, which will only run the specified code once so that you can validate multiple rows against it. The syntax is still not quite perfect, but it’s a big improvement over the first example above and avoids the multiple executions of the second example.
@SingleExecution ('calculatePayments') def "Should have implicit result field available for all tests"() { when: //result is implicitly created by the annotation def row = result[idx] then: row.date == date row.amount == amount where: idx | date | amount 0 | '2013-01-01' | 100.00 1 | '2013-02-01' | 150.00 //This closure gets called one time, regardless of the number of rows in the where clause def calculatePayments = { //... return myClass.getPayments() }
Admittedly, this is working around the framework a little bit, but it does provide a clean way to test tabular data returned from a single execution. There are many situations, especially when doing financial calculations where this makes testing much easier.
How to use @SingleExecution extesion with @DbUnit for tearDown operation DatabaseOperation.DELETE?