Groovy – Customising Power Assert messages

Power Assert messages are really useful when you’re trying to track down a problem using Groovy test cases. But what if you need just a little more information?

If you’re new to Groovy and you ever write tests you may take Power Assert messages for granted. You might not even know what they are.

By way of illustration let’s say we’re putting together a first pass Groovy unit test case for a bartender.  We kick off by seeing whether he can mix one of our favourite drinks:-

    void testMakeLongIslandIcedTea() {
  	    def drinkUnderTest = BarTender.makeLongIslandIcedTea()
	    LongIslandIcedTeaIngredients.each() { ingredient, expQuantity ->
		    assert drinkUnderTest.ingredients[ingredient].quantity 
				    == expQuantity;
	    }
    }

Our bartender fails this first test, and with Groovy 1.6 (before Power Asserts were added) we don’t get much information as to why:-

java.lang.AssertionError: Expression: 
    (drinkUnderTest.ingredients[ingredient].quantity 
         == expQuantity). Values: expQuantity = 50

Since we have multiple data points with the same expected value we can’t identify which data point this error relates to. Also, since the test fixture is data driven, we can’t use the stack trace to work it out either.

Fortunately later versions of Groovy give us much more information:-

Assertion failed:

assert drinkUnderTest.ingredients[ingredient].quantity == expQuantity
       |              |          ||           |        |  |
       |              |          |Gin         25       |  50
       |              |           (25 ml)              false
       |              [SourMix: (75 ml), TripleSec: (50 ml), Vodka: (50 ml), 
       |                   Gin: (25 ml), Tequila: (50 ml), Rum: (50 ml), 
       |                    Coke: (75 ml)]
       bar.LongIslandIcedTea

Now we can see our barman is giving half-measures of Gin, and mete out punishment accordingly.

When better isn’t enough

Our initial test case has some flaws. It presumes our barman is metric for a start, which is narrowing the field a bit, and it also demands exact measures when something reasonably close would be sufficient.

So we’ll refactor our test case, introducing a helper method to compare the expected and actual measures:-

    void testMakeLongIslandIcedTea() { 
	    def drinkUnderTest = BarTender.makeLongIslandIcedTea()
	    LongIslandIcedTeaIngredients.each() { ingredient, expQuantity ->
		    checkMeasure(drinkUnderTest.ingredients[ingredient].quantity, 
			    drinkUnderTest.ingredients[ingredient].measure,
			    expQuantity, Measure.ml)
	    }
    }

    void checkMeasure(observedQuantity, observedUnit, 
                      expectedQuantity, expectedUnit) {
	    def observed = observedQuantity * observedUnit.factor
	    def expected = expectedQuantity * expectedUnit.factor
	    assert 0.1 > Math.abs(1 - observed / expected)
    }

Our test fixture is still metric, but our bartender need no longer be, and we’re giving him a 10% margin of error.

We try out a new bartender, and he also fails the test:-

Assertion failed: 

assert 0.1 > Math.abs(1 - observed / expected)
                  |     | |        | |         |
                  0.5   | 25.0     | 50.0      false
                        0.5        0.5

We can see that we’re getting short measures again, but what of? Our assert statement doesn’t include any objects which uniquely identify the data point and again, since this is a data-driven fixture there are no clues from the stack trace.

The assert keyword accepts a second message parameter so we could pass something that uniquely identifies the data point into our helper method and use that to give us some more information.

    void testMakeLongIslandIcedTea() {
	    def drinkUnderTest = BarTender.makeLongIslandIcedTea()
	    LongIslandIcedTeaIngredients.each() { ingredient, expQuantity ->
		    checkMeasure(drinkUnderTest.ingredients[ingredient].quantity, 
			    drinkUnderTest.ingredients[ingredient].measure,
			    expQuantity, Measure.ml, 
                            ingredient)
	    }
    }

    void checkMeasure(observedQuantity, observedUnit, 
                      expectedQuantity, expectedUnit, ingredient) {
	    def observed = observedQuantity * observedUnit.factor
   	    def expected = expectedQuantity * expectedUnit.factor
	    assert 0.1 > Math.abs(1 - observed / expected), ingredient 
    }

Ordering the same again:-

java.lang.AssertionError: Rum. Expression: 
    (0.1 > java.lang.Math.abs((1 - (observed / expected))))

Ouch! One step forward and two steps back. Yes, we now know what the offending data point was, but where has all our other useful information gone? It would appear that including a custom message in an assert costs us the Power Assert output.

There doesn’t seem to be an easy way of generating Power Assert output ourselves. Fortunately though there is a simple workaround, which relies on the fact that a non-null string will always evaluate to true when part of an expression. Therefore we can always safely combine a custom message with the rest of our assertion and get it included in the output.

Refactoring out checkMeasure method with this in mind:-

    void checkMeasure(observedQuantity, observedUnit, expectedQuantity, 
                      expectedUnit, ingredient) {
        def observed = observedQuantity * observedUnit.factor
        def expected = expectedQuantity * expectedUnit.factor
        assert ingredient && 0.1 > Math.abs(1 - observed / expected)
    }

And ordering one for the road…

Assertion failed: 

assert ingredient && 0.1 > Math.abs(1 - observed / expected)
       |          |             |     | |        | |         |
       Rum        false         0.5   | 25.0     | 50.0      false
                                      0.5        0.5

Now we have all the information about the incorrect data point including the unique identifier since everything is part of the assertion we’re checking.

A simple workaround, though it might be worth including a comment explaining the idiom, since our test assertions sadly now include elements that aren’t actually under test.

Leave a Reply

Your email address will not be published. Required fields are marked *