JUnit recipes: a classic book on Object Testing

I recently read this book: JUnit recipes, by J.B. Rainsberger. This is a bit of an old-timer. Still, this is so far the most influential thing I read on unit testing. Earlier I read what the Pragmatic Programmers think about unit testing. I went to code retreats to learn TDD. Went to legacy code retreats to learn how to deal with an inherited mess. Applied those techniques in my work. Read innumerable blog posts on this topic and discussed them with colleagues. Still, I could learn a lot from this book.

What is an Object Test?

Most of us are familiar with the term Unit test. Unfortunately, we’re associating different meanings to this term:

Some people say that as long as you test one single unit then you’re writing unit tests. Then, they explain that one single unit can be anything from a method to a subsystem.

Other people say that unit testing is what you do with an unit testing tool. So, they say, if you use JUnit to invoke a web service and make assertions on the results, then, they say, you’re writing unit tests.

The term ‘Object Test’, however, has a narrower meaning. It implies that you test only one object in isolation.

What kind of tests should a developer write?

There is a simple answer to that question: A developer should write Developer Tests. This book calls them Programmer Tests, but that’s still a recursive definition.

Luckily, Rainsberger explains in this book that a programmer should write Object tests. Other people, like Devill think that a programmer should write small tests that check that objects work well together. This is actually a great question: Do we need to test single objects in isolation, or, we need to test a few objects together?

Rainsberger discusses this question here here. He talks about peers and internals as well as the Testing Pyramid.

According to the Testing Pyramid, you need lots of low-level tests and fewer high-level tests. High-level tests make sure that your code does the right thing while the low-level tests make sure that your code gets it right.

An example: calculator with history

Let’s create a calculator that keeps a history of the recent results. I borrowed this example from John Sonmez. He explains in this post that as you start to combine your objects together, your tests become difficult and brittle. Let’s see if we can prove otherwise!

Testing isolated objects

It’s very easy to test a single operation:

@Test
public void testAdding() {
    // GIVEN
    int a = 1;
    int b = 2;
    AddOperation operation = new AddOperation(a, b);

    // WHEN
    int sum = operation.calculate();

    // THEN
    assertEquals(3, sum);
}

@Test(expected=ArithmeticException.class)
public void testOverflow() {
    // GIVEN
    AddOperation operation = new AddOperation(Integer.MAX_VALUE, 2);

    // WHEN
    operation.calculate();
}

It’s similarly easy to test the storage of recent results:

@Test
public void test() {
    // GIVEN
    ResultStorage storage = new ResultStorage();

    // WHEN
    storage.add("2");
    storage.add("E");
    storage.add("3");

    // THEN
    assertEquals(Arrays.asList("2", "E", "3"), storage.getStoredResults());
}

Testing interactions

You can test whether certain events come after each other:

private ResultStorage storage;
private Calculator calculator;

@Before
public void setUp() {
    storage = new ResultStorage();
    calculator = new Calculator(storage);
}

@Test
public void testGeneralOperation() {
    // WHEN
    calculator.calculate(new PredefinedThreeOperation());

    // THEN
    assertEquals(Arrays.asList("3"), storage.getStoredResults());
}

@Test
public void testExceptionOperation() {
    // WHEN
    calculator.calculate(new ExceptionalOperation());

    // THEN
    assertEquals(Arrays.asList("E"), storage.getStoredResults());
}

This test is a little odd. First of all, it creates test-doubles for testing the calculate() method. These test doubles are simply simulating how an actual operation would behave. See, this is how we’re isolating one behavior from another.

Moreover, this is a bit of a white-box test. None of the clients should call the calculate() method directly. Still, we need a way to verify that it works well. We don’t need any false negative test failures if a particular operation starts to work differently. So, here I chose to make the calculate() method protected. To compensate this minor design deficiency, I added a comment that this method is protected only for testing reasons:

/** Protected for testing reasons */
protected void calculate(Operation operation) { /* actual implementation */ }

Testing and dependency injection

You can see that this test injects the ResultStorage into the calculator. This is an important concept, called Dependency Injection. It makes your code testable as well as it makes it easier to change. At first it might look a bit odd to put so much responsibility to the caller. Still, this design makes your code more flexible.

Suppose that you want to change the ResultStorage: you want it to actually store the results into a file. It shouldn’t be that hard:

  1. While keeping the ResultStorage interface, create a new ‘Production’ object, say, FileResultStorage. You might want to extract interface at this point.
  2. Using the same interface, create a test double, say SpyResultStorage.
  3. For the production code, inject the FileResultStorage. For testing, inject SpyResultStorage.

That’s it. Some folks say that using Dependency Injection makes your code testable. Others say that writing unit tests makes the design of your code more flexible. Either way, it’s a big win. Anyway, Dependency Injection is a very deep topic. There are entire books about it. If you want to learn more, then you might find this post interesting.

Testing more details

There is always little more that you can test. Rainsberger introduces the mantra ‘Write tests until fear turns into boredom.’ I was in the ‘fear’ mode so I added a test to check whether I’m creating the right operation:

@Test
public void testGettingTheRightOperation() {
    // GIVEN
    SpyAddOperationCalculator calculator = new SpyAddOperationCalculator();

    // WHEN
    calculator.add(2, 3);

    // THEN
    assertTrue(calculator.getAddOperationCalled);
}

@Test
public void testCallingCalculate() {
    // GIVEN
    SpyCalculateTemplateCalculator calculator = new SpyCalculateTemplateCalculator();

    // WHEN
    calculator.add(1, 2);

    // THEN
    assertTrue(calculator.calledWithRightOperation);

These are the related spy classes:

private static class SpyAddOperationCalculator extends Calculator {

    public SpyAddOperationCalculator() {
        super(new ResultStorage());
    }

    private boolean getAddOperationCalled;

    @Override
    protected Operation getAddOperation(int a, int b) {
        getAddOperationCalled = true;
        return null;
    }

}

private static class SpyCalculateTemplateCalculator extends Calculator {

    private boolean calledWithRightOperation;

    public SpyCalculateTemplateCalculator() {
        super(null);
    }

    @Override
    protected void calculate(Operation operation) {
        calledWithRightOperation = operation instanceof AddOperation;
    }

}

These testing subclasses are still a bit odd: they overwrite some methods of the original object. This might be a test smell. Maybe you need to extract some methods into a new class. Maybe, as John Sonmez suggests in the original post, we need to create a Mediator that calls both a calculator and a storage.

Let’s put the design issues aside. This Testing Subclass is a useful technique anyway. It can become handy when dealing with legacy code. This sort of tests offer a safety net for refactoring. All we need to keep in mind is that if tests are in our way, we can always delete or rewrite them.

Testing the component

All we need is a simple smoke test:

private ResultStorage storage;
private Calculator calculator;

@Before
public void setUp() {
    storage = new ResultStorage();
    calculator = new Calculator(storage);
}

@Test
public void addTest() {
    // WHEN
    calculator.add(1, 2);
    calculator.add(-5, 1);

    // THEN
    assertEquals(Arrays.asList("3", "-4"), calculator.getResults());
}

Simple indeed. But hey, the goal of this test is just to see whether we’re creating the right thing. We already checked if we got it right.

You can find the project here. It has all the tests and production code together.

Wrapping up

I only scratched the surface in this post.

The book is very thorough. It shows you various techniques for testing things in isolation. It goes through all the possible problems that you can meet with Java SE and EE. It discusses how you can organize your tests. It even gives useful advice on how to deal with legacy code. No wonder – the author is the same Rainsberger who wrote the trivia game for legacy code retreats.

One might criticize this book for being an old-timer. E.g. it uses JUnit 3 and it shows how to test EJB 2.x. Still, it has so many relevant techniques that I can only recommend it.

About tamasrev

Java bugrammer, reader, writer. Father, sometimes mother.
This entry was posted in programming and tagged , , , , . Bookmark the permalink.

Leave a comment