Tuesday, March 10, 2009

Micro DSL meets testing

Last night I was writing a number of tests. All the tests were quite similar, so there was potential for much duplication. An example of one of the tests was this:
@Test
public void exampleTest() {
context.checking(new Expectations(){{
one(serviceA).getValue(); will(returnValue(SMALL_NUMBER));
one(serviceB).getValue(); will(returnValue(BIG_NUMBER));
}});
assertThat(new ValueChooser().chooseFrom(serviceA.getValue(),
serviceB.getValue()),
is(equalTo(SMALL_NUMBER);
}

I basically needed to assert the result of the value chooser, based on what the 2 services returned. So, to avoid duplication, instinct tells me to extract method, and parameterise variable parts. This would give you a method like so:
assertValueChosen(serviceAResponse, serviceBResponse, expectedValue);

When looking at this, it is not too difficult to tell what the parameters are, but if you actually place values into the parameter list, it can detract from the readability of the assert. For example:
assertValueChosen(SMALL_NUMBER, BIG_NUMBER, SMALL_NUMBER);

The name of the test could describe what is being asserted, and yes, this should definitely be correct because this is what is output in the JUnit results (and thus reported on), but it is quite easy (like it is with comments) to change the code, and have the name of the tests method no longer describe exactly what it is that test is testing. The name of the test could be out of date.

I had a small think about it, and decided that, what I would like would be a sentence that read like an example of behaviour. "When service A returns x, and service B returns y, then assert the value chosen is z" kind of thing.

What I decided on was to use a Micro DSL. Doing so allowed me to express my tests more clearly and not have to rely on the name of the test:
whenServiceAReturns(SMALL_NUMBER).andServiceBReturns(BIG_NUMBER).
assertValueChosenIs(SMALL_NUMBER);

Lovely! It is now impossible for this sentence to be out of date, from what is being executed.

So now, for your viewing pleasure, here is the whole test in its entirety...
public class ValueChooserTest {

private static final int SMALL_NUMBER = 5;
private static final int BIG_NUMBER = 10;

@Test
public void testAllCases() {
whenServiceAReturns(SMALL_NUMBER).andServiceBReturns(BIG_NUMBER).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(BIG_NUMBER).andServiceBReturns(SMALL_NUMBER).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(null).andServiceBReturns(SMALL_NUMBER).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(SMALL_NUMBER).andServiceBReturns(null).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(null).andServiceBReturns(null).assertValueChosenIs(0);
}

private class ValueChooserAsserter {
private Integer serviceAValue;
private Integer serviceBValue;

private final Mockery context = new JUnit4Mockery();
private final Service serviceA = context.mock(Service.class, "serviceA");
private final Service serviceB = context.mock(Service.class, "serviceB");

private ValueChooserAsserter(Integer serviceAValue) {
this.serviceAValue = serviceAValue;
}

public static ValueChooserAsserter whenServiceAReturns(Integer value) {
return new ValueChooserAsserter(value);
}

public ValueChooserAsserter andServiceBReturns(Integer serviceBValue) {
this.serviceBValue = serviceBValue;
return this;
}

public void assertValueChosenIs(Integer expectedValue) {
context.checking(new Expectations(){{
one(serviceA).getValue(); will(returnValue(serviceAValue));
one(serviceB).getValue(); will(returnValue(serviceBValue));
}});
assertThat(new ValueChooser().chooseFrom(serviceA.getValue(), serviceB.getValue()), is(equalTo(expectedValue)));
}
}

}

So people, what do you think? :)

---------- UPDATE ----------

Here is the example modified after Romilly's comments:

public class ValueChooserTest {

private static final int SMALL_NUMBER = 5;
private static final int BIG_NUMBER = 10;

@Test
public void testAllCases() {
whenServiceAReturns(SMALL_NUMBER).andServiceBReturns(BIG_NUMBER).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(BIG_NUMBER).andServiceBReturns(SMALL_NUMBER).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(null).andServiceBReturns(SMALL_NUMBER).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(SMALL_NUMBER).andServiceBReturns(null).assertValueChosenIs(SMALL_NUMBER);
whenServiceAReturns(null).andServiceBReturns(null).assertValueChosenIs(0);
}

private class ValueChooserAsserter {
private Integer serviceAValue;
private Integer serviceBValue;

private final Mockery context = new JUnit4Mockery();
private final Service serviceA = context.mock(Service.class, "serviceA");
private final Service serviceB = context.mock(Service.class, "serviceB");

private ValueChooserAsserter(Integer serviceAValue) {
this.serviceAValue = serviceAValue;
}

public static ValueChooserAsserter whenServiceAReturns(Integer value) {
return new ValueChooserAsserter(value);
}

public ValueChooserAsserter andServiceBReturns(Integer serviceBValue) {
this.serviceBValue = serviceBValue;
return this;
}

public void assertValueChosenIs(Integer expectedValue) {
context.checking(new Expectations(){{
one(serviceA).getValue(); will(returnValue(serviceAValue));
one(serviceB).getValue(); will(returnValue(serviceBValue));
}});
assertThat(new ValueChooser(serviceA, serviceB).choose(), is(equalTo(expectedValue)));
}
}

}

4 comments:

  1. nice one!

    (god what have I created! :) )

    ReplyDelete
  2. I like the DSL. However -

    I'm being very slow here, I guess. Why do you need the services and their mocks in this test?

    All you're testing is the ValueChooser,and all that needs is a pair of values. Where they come from does not matter.

    ReplyDelete
  3. Indeed. I was getting a little mock happy here. I will post a simpler version in a bit.

    ReplyDelete