Writing unit tests can be a boring and time consuming task. Especially for beginners it is sometimes impossible to see the light at the horizon when they start to write tests.
The problem is that only a small set of third party libs where really implemented by using TDD. Those libs which do not have a sufficient test coverage are by definition "Legacy Code". The same is true for the code which is written against those APIs without a layer which encapsulates this lagacy code.
Not a single project I know has actually started on a green field. We are happy to have a rich base of third party libs which are supposed to do our work quickly. And that is probably true until it comes to testing.
Writing a test against code which is forced to use legacy code is a pain. The result is often a complex unit test with tons of mocks and assumptions which are them selfs not easy to understand or even readable. Our logic which needs to test against is often hidden in the test. In the end the unit test is useless as it is not readable and maintainable. But we need to have tests which can be refactored without breaking existing functionality. Most of the time the implementation of tests do not reflect the business case for which they are written.
So we have a problem with readability and maintainability of tests. We could use tools like FIT or concordion to solve the problem at a higher level by moving out the business rules from our tests and put the rules in a verbose way into external documents. I think these tools are great if you find someone (the costumer?) who is willing to write such details you need to write the tests against. That is probably working for large scaled projects. But even there I could imagine that it is really hard to find someone to write those documents. At the end the developer who does the implementation is forced to write and maintain the external documents.
I think the approach of FIT and concordian is really great. But why not writing the business rules into our unit tests instead to maintain them in external documents. If we could find a way to express these rules in our unit tests in the same way as we would do in such documents we would save a lot of work. And last but not least we always have more power in the source code than in a document which only understands text. A complex mapping between the documents and the real test code is also not necessary.
We already have everything in place in our programming language!
Inspired by the interview of Adam Bien where he showed how we could simply play with the builder pattern, I thought it would be a nice idea to express our business rules in unit tests in the same way!
Here is the part of the interview which I found really impressing:
training.login(userName,password).
running().
today().
weather(SUN).
averagePulse(135).
comment("ok").
length(65).
maxPulse(165).
km(12).
add();
So the simple part is now to write our business rules in such a way in a unit test!
Lets take the (simplified) example from the concordion site:
"User should be able to remove items from unshipped orders"
Can be expressed in this way in a unit test:
public class OrderTest {
@Test
public void testAllowRemoveItemsFromUnshippedOrder() {
order()
.withItems()
.unshipped()
.canRemoveItems()
.hasTotal(0);
}
@Test
public void testCannotRemoveItemsFromShippedOrder() {
order()
.withItems()
.shipped()
.canNotRemoveItems();
}
@Test
public void testTotalOfItemsIsTotalOfOrder() {
order()
.withItems()
.hasTotal(totalOfItems());
}
@Test
public void testEmptyOrderHasTotalOfZero() {
order()
.hasTotal(0);
}
private Order _order;
private List- _items;
private OrderTest order() {
_order = new Order();
}
private OrderTest withItems() {
_items = new ArrayList- ();
_items.add(itemWithPrice(20));
_items.add(itemWithPrice(40));
_order.addItems(_items);
return this;
}
private double sumOfPrices(Iterable- items) {
double sum = 0;
for(Item item : items) {
sum += item.getPrice();
}
return sum;
}
private double totalOfItems() {
return sumOfPrices(_items);
}
private OrderTest hasTotal(double expectedTotal) {
assertThat(order.getTotal(), equalTo(expectedTotal));
return this;
}
private OrderTest unshipped() {
return shipped(false);
}
private OrderTest shipped() {
return shipped(true);
}
private OrderTest shipped(boolean shipped) {
_order.setShipped(shipped);
assertThat(_order.isShipped(), equalTo(shipped))
return this;
}
private OrderTest canRemoveItems() {
_order.removeItems(_items);
assertThat(order.isEmpty(), equalTo(true));
return this;
}
private OrderTest canNotRemoveItems() {
try {
_order.removeItems(_items);
fail("Exception expected while removing items from order " + _order);
} catch (Exception e) {
// expected
}
return this;
}
}
The complex stuff which is needed to set up the test fixture, to mock contributing components or to verify expected behavior can be hidden in properly named methods. This boosts readability and maintainability when the use case which we are testing here changes or varies at a later time. On the other side when we need to refactor the code we are not forced to change the test methods which contains the definition of the use case. We are also able to extend the tests by varying the assumptions or expectations.
