Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Friday, April 20, 2012

Yet More Spock Magic: Mocks

Spock's built-in mock object capabilities are just a dream to use ... unlike other systems I've used, it doesn't get in your way, or force you to think backwards or inside out. Once again, some listings. These are for tests of Tapestry IoC's AspectDecorator service, which is used to create a wrapper interceptor around some other object. The test below shows how a supplied MethodAdvice callback object is invoked by the interceptor, if the advice is associated with the invoked method.

TestNG with EasyMock (Java)

public class AspectInterceptorBuilderImplTest extends IOCInternalTestCase
{
private AspectDecorator decorator;
@BeforeClass
public void setup()
{
decorator = getService(AspectDecorator.class);
}
public interface Subject
{
void advised();
void notAdvised();
}
@Test
public void some_methods_not_intercepted() throws Exception
{
Subject delegate = newMock(Subject.class)
MethodAdvice advice = new MethodAdvice()
{
public void advise(MethodInvocation invocation)
{
assertEquals(invocation.getMethod().getName(), "advised");
invocation.proceed();
}
};
delegate.advised();
delegate.notAdvised();
replay();
AspectInterceptorBuilder<Subject> builder = decorator.createBuilder(Subject.class, delegate, "<Subject>");
builder.adviseMethod(Subject.class.getMethod("advised"), advice);
Subject interceptor = builder.build();
interceptor.advised();
interceptor.notAdvised();
verify();
}
}
Even this example is a bit streamlined, as some of the mock object capabilities, such as methods newMock(), replay() and verify() are being derived from the TestBase base class.

Spock

interface InterceptorSubject {
void advised()
void notAdvised()
}
class AspectInterceptorBuilderImplSpec extends AbstractSharedRegistrySpecification {
@Shared
private AspectDecorator decorator
def setupSpec() {
decorator = getService AspectDecorator
}
def "ensure that non-advised methods are not passed through the MethodAdvice object"() {
InterceptorSubject delegate = Mock()
MethodAdvice advice = Mock()
def builder = decorator.createBuilder(InterceptorSubject, delegate, "<InterceptorSubject>")
builder.adviseMethod(InterceptorSubject.getMethod("advised"), advice)
InterceptorSubject interceptor = builder.build()
when:
interceptor.advised()
then:
1 * advice.advise(_) >> { MethodInvocation mi ->
assert mi.method.name == "advised"
mi.proceed()
}
1 * delegate.advised()
0 * _
when:
interceptor.notAdvised()
then:
1 * delegate.notAdvised()
0 * _
}
}

Spock's wonderful when: / then: blocks organize the behavior into a stimulus followed by a response; using EasyMock, you have to train the mock objects for the response before introducing the stimulus (the method invocation). Further, with EasyMock there's one API for methods that return a fixed response, a second API for methods that throw an exception, and a third API for methods where the result must be calculated; in Spock the value after the >> operator is either a literal value, or a closure that can do what it likes, such as the one attached to MethodAdvice.advice() that checks for the expected method name, and then proceed()s to the delegate mock object's method.

I think that a reasonable developer, even without a thorough understanding of Spock, would get the gist of what this test does (perhaps with a little side note about the interaction system inside the then: block). On the other hand, I've seen when training people with TestNG and EasyMock that it very rarely sinks in immediately, if at all.

Thursday, April 19, 2012

Yet Another Bit of Spock Love

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)

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.