Monday, March 23, 2009

Expressing time as a domain object, instead of insane long calculations


Hands up everyone here who has had to represent time as a long? OK, next question, how many of you have had to express a large amount of time (for example 7 days) in milliseconds? I bet you've done this:
7*24*60*60*1000

Kind of nice, and I bet you've even refactored like this:
HOURS_IN_A_DAY*MINUTES_IN_AN_HOUR*SECONDS_IN_A_MINUTE*MILLISECONDS_IN_A_SECOND

OK, getting better. It's a bit clearer as to what all those troublesome little numbers mean-but something still not quite right. What if, say for example, you pass this result into a method that accepts time in a different unit of measure? eg.
void sleepFor(long seconds) {
Thread.sleep(seconds*1000);
}

Now the compiler isn't going to complain, you're going to be waiting a lot longer than you anticipated-say 1000 times longer? We've passed the wrong unit of time into this method. Our constant is represented in milliseconds, yet this method accepts time as seconds, but they are both the same datatype! But, I hear you say, we don't want a different datatype for every unit of time-that would be madness. Indeed it would.

To stop the world descending into a pit of chaos, overlorded by mad monkeys*, try this. Don't represent time as a long, represent the amount of time you are interested in as a long, and combine it with a TimeUnit!

Call it what you like, but I called it Duration. An example of which can be found in a project I'm currently working on called gibble here. This is only a small example of the Duration class that I use elsewhere.

Now, we can represent the above code as follows:
sleepFor(days(7)); //static import of Duration here

And the sleepFor method like this:
void sleepFor(Duration timeout) {
Thread.sleep(timeout.inMillis());
}

What's so cool is that we can never be tricked into sleeping for the wrong amount of time! I hate being tricked into sleeping 1000 times more that I have too. Sleep too much and the monkeys might take over!

* Would that be any worse/different that what we have now?

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)));
}
}

}