Make your unit tests clean – Part I, The Test Builder Pattern

Have you ever heard that saying

unit tests should act as your documentation

Have you ever struggled with your task because you weren’t quite sure how a class Foo works? Well, I bet you have.

The first thing you should then do is reading your test suite (if you have one…). After a few minutes, you should at least have an insight how it behaves.

So the lesson is: not only should your unit test suite prove your software works, but readability is also an important concept.

So what is this post about?

Imagine a simple case: we have a TicketMachine. TicketMachine can give you a ticket for a very scary movie, totally for free! The only rule is you need to be at least 18 years old.

In java:

public class ScaryMovieTicketMachine {

    public Ticket giveTicket(Customer customer) {
        Preconditions.checkArgument(!customer.isUnderAge(), "You're not allowed to watch this movie!");

        return new Ticket();
    }
}
public class Customer {

    private final String firstName;
    private final String lastName;
    private final int age;
    private final int weight;
    private final Sex sex;
    private final CustomerAddress address;


    public Customer(String firstName, String lastName, int age, int weight, Sex sex, CustomerAddress address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.weight = weight;
        this.sex = sex;
        this.address = address;
    }

    public boolean isUnderAge() {
        return age < 18;
    }
}

How would you test a failure case? Well, using junit, you will probably create a similar test case:

@Test(expected = IllegalArgumentException.class)
public void shouldNotGiveTicketGivenUnderAgeCustomer() throws Exception {

    final ScaryMovieTicketMachine objectUnderTest = new ScaryMovieTicketMachine();

    final Customer underAgeCustomer = new Customer(
            "john", "doe", 15, 60, Sex.MALE, new CustomerAddress() // not so important
    );

    objectUnderTest.giveTicket(underAgeCustomer);

}

So what’s wrong with this test? Well, actually nothing. Mainly because the „business logic” is trivial. Of course you can (and maybe should) extract all those literals into variables with proper name.

But still, a lot of useless  (in this case) params we need to provide to Customer’s constructor.

The question is: should we? We’re only concerned here with customer age – why should we even care about other parameters in this test case? Can we improve it somehow? Yeah, of course we can!

Factories to the rescue?

One pattern to solve this problem is to use test-specific factories. I mean, something like:

class CustomerTestFactory {
    public static Customer underAgeCustomer() {
        ....
    }
}

This is a nice approach. The problem is, once you need several configurations, it starts to grow.

Build, build, build…

And now, wait for a second… In an example above, we only care about one particular property. But there might be a case when we’re interested in Customer’s firstName and lastName only. Does it sound familiar? Indeed, it does! It’s a builder pattern…

….but slightly modified.

Here’s the solution:

public class CustomerBuilder {

    private String firstName = "default";
    private String lastName = "default";
    private int age = 18;
    private int weight = 60;
    private Sex sex = Sex.MALE;
    private CustomerAddress address = new CustomerAddress();
    
    
    private CustomerBuilder() {}
    
    public static CustomerBuilder newCustomer() {
        return new CustomerBuilder();
    }

    public CustomerBuilder firstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public CustomerBuilder lastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

    public CustomerBuilder age(int age) {
        this.age = age;
        return this;
    }

    public CustomerBuilder weight(int weight) {
        this.weight = weight;
        return this;
    }

    public CustomerBuilder sex(Sex sex) {
        this.sex = sex;
        return this;
    }

    public CustomerBuilder address(CustomerAddress address) {
        this.address = address;
        return this;
    }
    
    public Customer build() {
        return new Customer(firstName, lastName, age, weight, sex, address);
    }
}

And the usage:

@Test(expected = IllegalArgumentException.class)
public void shouldNotGiveTicketGivenUnderAgeCustomer() throws Exception {

    final ScaryMovieTicketMachine objectUnderTest = new ScaryMovieTicketMachine();

    final Customer underAgeCustomer = CustomerBuilder
            .newCustomer()
            .age(17)
            .build();

    objectUnderTest.giveTicket(underAgeCustomer);

}

You may argue whether it is more readable or not (IMO it is), but there is one more important thing: if you change Customer constructor somehow, the only place you should fix to make your test suite work again is your builder class!

In case you just thought: JESUS FUCKING CHRIST, NOT THAT JAVA BOILERPLATE AGAIN…

…calm down, there is a solution for that:

http://projectlombok.org/

And this is how our builder looks like using it:

@Setter
@Accessors(fluent = true)
@NoArgsConstructor(staticName = "newCustomer")
public class CustomerBuilder {

    private String firstName = "default";
    private String lastName = "default";
    private int age = 18;
    private int weight = 60;
    private Sex sex = Sex.MALE;
    private CustomerAddress address = new CustomerAddress();

    public Customer build() {
        return new Customer(firstName, lastName, age, weight, sex, address);
    }
}

Conclusion A.K.A. „Why should I even care?” A.K.A „tl;dr”

I encourage you to use this pattern whenever you can.

Why? Well, mainly because:

  • you’re (hopefully) not the only one who’s reading your (test) code
  • it makes your tests more readable and allows you to focus only on important things, without unnecessary random literals cluttering it
  • it makes your test code more decoupled from actual implementation and makes it harder to break

That’s all for today, please share your opinion in comments 😉

Reklamy
Zwykły wpis