Creator of the Apache Tapestry web application framework and the Apache HiveMind dependency injection container. Howard has been an active member of the Java community since 1997. He specializes in all things Tapestry, including on-site Tapestry training and mentoring, but has lately been spreading out into fun new areas including functional programming (with Clojure), and NodeJS. Howard is a DZone MVB and is not an employee of DZone and has posted 79 posts at DZone. You can read more from them at their website. View Full User Profile

Yet Another Bit of Spock Love

04.30.2012
| 2654 views |
  • submit to reddit

I'm gradually converting a back-log of existing tests to Spock ... and some of them convert so beautifully, it hurts. Here's an example:

Before (Java and TestNG)

public class CronExpressionTest extends Assert
{
    /*
    * Test method for 'org.quartz.CronExpression.isSatisfiedBy(Date)'.
    */
    @Test
    public void testIsSatisfiedBy() throws Exception
    {
        CronExpression cronExpression = new CronExpression("0 15 10 * * ? 2005");

        Calendar cal = Calendar.getInstance();

        cal.set(2005, Calendar.JUNE, 1, 10, 15, 0);
        assertTrue(cronExpression.isSatisfiedBy(cal.getTime()));

        cal.set(Calendar.YEAR, 2006);
        assertFalse(cronExpression.isSatisfiedBy(cal.getTime()));

        cal = Calendar.getInstance();
        cal.set(2005, Calendar.JUNE, 1, 10, 16, 0);
        assertFalse(cronExpression.isSatisfiedBy(cal.getTime()));

        cal = Calendar.getInstance();
        cal.set(2005, Calendar.JUNE, 1, 10, 14, 0);
        assertFalse(cronExpression.isSatisfiedBy(cal.getTime()));
    }

    @Test
    public void testLastDayOffset() throws Exception
    {
        CronExpression cronExpression = new CronExpression("0 15 10 L-2 * ? 2010");

        Calendar cal = Calendar.getInstance();

        cal.set(2010, Calendar.OCTOBER, 29, 10, 15, 0); // last day - 2
        assertTrue(cronExpression.isSatisfiedBy(cal.getTime()));

        cal.set(2010, Calendar.OCTOBER, 28, 10, 15, 0);
        assertFalse(cronExpression.isSatisfiedBy(cal.getTime()));

        cronExpression = new CronExpression("0 15 10 L-5W * ? 2010");

        cal.set(2010, Calendar.OCTOBER, 26, 10, 15, 0); // last day - 5
        assertTrue(cronExpression.isSatisfiedBy(cal.getTime()));

        cronExpression = new CronExpression("0 15 10 L-1 * ? 2010");

        cal.set(2010, Calendar.OCTOBER, 30, 10, 15, 0); // last day - 1
        assertTrue(cronExpression.isSatisfiedBy(cal.getTime()));

        cronExpression = new CronExpression("0 15 10 L-1W * ? 2010");

        cal.set(2010, Calendar.OCTOBER, 29, 10, 15, 0); // nearest weekday to last day - 1 (29th is a friday in 2010)
        assertTrue(cronExpression.isSatisfiedBy(cal.getTime()));

    }
}

After (Spock)

@Unroll
class CronExpressionSpec extends Specification {

  def propertyMissing(String name) { Calendar[name] }

  def "isSatisfiedBy(#year, #month, #day, #hour, #minute, #second ) should be #satisfied for expression '#expr'"() {

    def cal = Calendar.getInstance();

    def exp = new CronExpression(expr)

    cal.set year, month, day, hour, minute, second

    expect:

    exp.isSatisfiedBy(cal.time) == satisfied

    where:
    expr                    | year | month   | day | hour | minute | second | satisfied
    "0 15 10 * * ? 2005"    | 2005 | JUNE    | 1   | 10   | 15     | 0      | true
    "0 15 10 * * ? 2005"    | 2006 | JUNE    | 1   | 10   | 15     | 0      | false
    "0 15 10 * * ? 2005"    | 2005 | JUNE    | 1   | 10   | 16     | 0      | false
    "0 15 10 * * ? 2005"    | 2005 | JUNE    | 1   | 10   | 14     | 0      | false
    "0 15 10 L-2 * ? 2010"  | 2010 | OCTOBER | 29  | 10   | 15     | 0      | true
    "0 15 10 L-2 * ? 2010"  | 2010 | OCTOBER | 28  | 10   | 15     | 0      | false
    "0 15 10 L-5W * ? 2010" | 2010 | OCTOBER | 26  | 10   | 15     | 0      | true
    "0 15 10 L-1 * ? 2010"  | 2010 | OCTOBER | 30  | 10   | 15     | 0      | true
    "0 15 10 L-1W * ? 2010" | 2010 | OCTOBER | 29  | 10   | 15     | 0      | true
  }
}

What a difference; the data-driven power of the where: block makes this stuff a bit of a snap, and you can see in once place, at a glance, what's going on. IDEA even lines up all the pipe characters automatically (wow!). It's obvious how the tests execute, and easy to see how to add new tests for new cases. By comparison, the TestNG version looks like a blob of code ... it takes a bit more scrutiny to see exactly what it is testing and how.

In addition, the propertyMissing() trick means that any property (including public static fields) of Calendar is accessible without qualification, making things look even nicer. This is what they mean by writing an "executable specification", rather than just writing code.

I can't say this enough: using any other framework for testing Java or Groovy code would simply not be logical.

 

Published at DZone with permission of Howard Lewis Ship, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Nicolas Frankel replied on Tue, 2012/05/01 - 2:24am

Your example is unbalanced, you're not using TestNG provider methods. I encourage you to look at them, adapt your example and then you'll see, the difference is not so extreme. You'll also decouple the data part from the test part for free.

Howard Lewis Ship replied on Tue, 2012/05/01 - 10:52am

@Nicolas,

If you look at Tapestry's TestNG test suite, you'll see that  I make extensive use of provider methods; for all I know, the "where:" block in Spock is based on this clever idea.  That being said, what Spock is bringing to the table is a much more complete synthesis:  having the data in the same method encapsulates the Spock concept of a feature method (a method that tests a single feature), the @Unroll annotation ensures that exceptions and generated reports are meaningful (the data being tested is incorporated into the test case name used in reports), and the tabular formatting is much, much, easier to read than nested Object arrays in TestNG.

 If you look on my blog (and shortly, I'd bet, on JavaLobby), I have a follow on where I show examples of Spock's integrated mock object support, which is also terrific.

 TestNG  does about as well as you can for a Java-oriented framework, within the confines of the Java language. Spock exploits the dynamic nature of Groovy, including AST transforms, to go much further than TestNG can.\

That being said, Spock inherits some limitations from JUnit that cause me some grief. I do love how TestNG breaks the overall test execution into a Suite containing Tests containing Test Cases, and provides before and after calls back at all three levels. 

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.