Somewhere right now a developer is adding comments to their test case.

It’s admirable. It’s a desire to improve the readability of their code.

But I know of a way that can improve the test to a far greater extent. Which also acts as a feedback loop on the design of your code.

But first, let us have a look at the state of the commenting art.

Test case commenting comes in a few variations that are all ultimately the same. There’s the Dan North inspired Given, When, Then (GWT) version:

[TestMethod]  
public void Withdraw_ValidAmount_ChangesBalance()  
{  
    // Given  
    double currentBalance = 10.0;  
    double withdrawal = 1.0;  
    double expected = 9.0;  
    var account = new CheckingAccount("JohnDoe", currentBalance);  

    // When  
    account.Withdraw(withdrawal);  
    double actual = account.Balance;  

    // Then  
    Assert.AreEqual(expected, actual);  
}  

The Bill Wake inspired Triple A of Arrange, Act, Assert (3A):

it('should do something', function() {
  //arrange...
  var dummyData = { foo: 'bar' };
  var expected = 'the result we want';
 
  //act...
  var result = functionUnderTest(dummyData);
 
  //assert...
  expect(result).to.equal(expected);
});

And sometimes people take it to the extreme by introducing an actual DSL for those comments:

describe Stack do
  context "with one item" do
     Given(:stack) { Stack.new }

    context "when popping" do
      When(:pop_result) { stack.pop }

      Then { pop_result == :an_item }
      Then { stack.depth == 0 }
    end
  end
end

These names all pay homage to Tony Hoare’s formal logic for proving that a program is correct. It comes in three sections:

  • pre-conditions
  • command
  • post-conditions

Call it GWT or 3A; it doesn’t change the anatomy of the test. All test cases follow this structure to prove their creator’s programs are correct.

That’s my first small issue with the comments that are creeping in. They are superfluous. Every unit test you see has the same three sections. Setting up state, performing an action then looking at the result or for a side effect. Every test has this anatomy. There’s no need to point it out in hundreds of comments.

So what’s a better approach? Just drop the comments?

Nope. Not just drop them. Those comments improve readability when it’s difficult to see where the different sections of the test lay. So let’s attack the need for that by introducing a rule:

I’m going to give you a budget of two blank lines maximum in your test cases.

See that set up state code spaced out over multiple lines? Not allowed.
See the assertions spaced out over multiple lines? Not allowed.

When you remove the extra blank lines all you are left with is a wall of code.

It’s ugly. This is great news.

When you see the code is ugly, it encourages you to improve it. We’ll explore an example in a minute as you might be thinking why on earth you’d want to adopt this rule.

The two blank line budget is an enabling constraint. Enabling constraints are about opening up opportunities by limiting our choices.

There was once a challenge for writers to sum up their life in six words:

I picked passion. Now I’m poor. – [Kathleen E. Whitlock]

Alzheimer’s: meeting new people every day. – [Phil Skversky]

Without these constraints, they’d never create these short pieces of work. It’s about constraining breadth to get focus and depth. And that is precisely what the two blank line budget gives you.

Time to show you the money. Here is a typical example using the GWT format.

@Test
public void shouldDeliverCargoToDestination() {
    // Given
    Driver driver = new Driver("Teddy");
    
    Cargo cargo = new Cargo();
    
    Position destinationPosition = new Position("52.229676", "21.012228");
    
    Truck truck = new Truck();
    truck.setDriver(driver);
    truck.load(cargo);
    truck.setDestination(destinationPosition);

    // When
    truck.driveToDestination();

    // Then
    assertEquals(destinationPosition, truck.getCurrentPosition());
}

Our first step is to remove the blank lines between the Given statements. Then drop the comments. This leaves us with a wall of state setup.

@Test
public void shouldDeliverCargoToDestination() {
    Driver driver = new Driver("Teddy");
    Cargo cargo = new Cargo();
    Position destinationPosition = new Position("52.229676", "21.012228");
    Truck truck = new Truck();
    truck.setDriver(driver);
    truck.load(cargo);
    truck.setDestination(destinationPosition);

    truck.driveToDestination();

    assertEquals(destinationPosition, truck.getCurrentPosition());
}

We’ve now made it ugly. So let’s react to that.

I hardly ever use the Builder pattern for production code, but it’s terrific for test code. It lets you fluently express object construction, leaving out unimportant details.

Let’s move the state setup into a Builder and see what we have:

@Test
public void shouldDeliverCargoToDestination() {
    Truck truck = new TruckBuilder()
                .driver("Teddy")
                .destination("52.229676", "21.012228")
                .build();

    truck.driveToDestination();

    assertEquals(new Position("52.229676", "21.012228"), truck.getCurrentPosition());
}

Better right? You could argue that you don’t even need to know the driver’s name for this test. Making it leaner. In the previous test, every value seemed necessary, in this one, it’s clear what is going on.

That’s what the two blank line constraint gives you. It forces you to deal with complex parts of your code, which were cosmetically fixed by adding extra blank spaces and comments.

Your tests are speaking to you now, are you going to listen?

Related Articles