This is not intended to be a heavily technical post. The goal of this post is to give you some guidelines to make your JUnit testing life more easy, to enable you to write complex scenarios for tests in minutes with the bonus of having extremely readable tests.
There are two major parts in a Unit tests that require writing a lot of bootstrap code:
- the setup part: constructing your initial state requires building the initial objects that will be fed to your SUT (system under test)
- the assertion part: constructing the desired image of your output objects and making assertions only on the needed data.
In order to reduce the complexity of building objects for tests I suggest using the Builder pattern in the following interpretation:
Here is the domain object:
public class Employee {
private int id;
private String name;
private Department department;
//setters, getters, hashCode, equals, toString methods
The builder for this domain object will look like this:
public class EmployeeBuilder {
private Employee employee;
public EmployeeBuilder() {
employee = new Employee();
}
public static EmployeeBuilder defaultValues() {
return new EmployeeBuilder();
}
public static EmployeeBuilder clone(Employee toClone) {
EmployeeBuilder builder = defaultValues();
builder.setId(toClone.getId());
builder.setName(toClone.getName());
builder.setDepartment(toClone.getDepartment());
return builder;
}
public static EmployeeBuilder random() {
EmployeeBuilder builder = defaultValues();
builder.setId(getRandomInteger(0, 1000));
builder.setName(getRandomString(20));
builder.setDepartment(Department.values()[getRandomInteger(0, Department.values().length - 1)]);
return builder;
}
public EmployeeBuilder setId(int id) {
employee.setId(id);
return this;
}
public EmployeeBuilder setName(String name) {
employee.setName(name);
return this;
}
public EmployeeBuilder setDepartment(Department dept) {
employee.setDepartment(dept);
return this;
}
public Employee build() {
return employee;
}
}
As you can see we have some factory methods:
public static EmployeeBuilder defaultValues()
public static EmployeeBuilder clone(Employee toClone)
public static EmployeeBuilder random()
These methods return different builders:
- defaultValues : some hardcoded values for each fields ( or the Java defaults – current implementation)
- clone : will take all the values from the initial object, and give you the possibility to change just some
- random : will generate random values for each field. This is very useful when you have a lot of fields that you don’t specifically need in your test, but you need them to be initialized. getRandom* methods are defined statically in another class.
You can add other methods that will initialized your builder accordingly to your needs.
Also the builder can handle building some objects that are not so easily constructed and changed. For example let’s change a little bit the Employee object and make it immutable:
public class Employee {
private final int id;
private final String name;
private final Department department;
...
}
Now we lost the possibility to change the fields as we wish. But using the builder in the following form we can regain this possibility when constructing the object:
public class ImmutableEmployeeBuilder {
private int id;
private String name;
private Department department;
public ImmutableEmployeeBuilder() {
}
public static ImmutableEmployeeBuilder defaultValues() {
return new ImmutableEmployeeBuilder();
}
public static ImmutableEmployeeBuilder clone(Employee toClone) {
ImmutableEmployeeBuilder builder = defaultValues();
builder.setId(toClone.getId());
builder.setName(toClone.getName());
builder.setDepartment(toClone.getDepartment());
return builder;
}
public static ImmutableEmployeeBuilder random() {
ImmutableEmployeeBuilder builder = defaultValues();
builder.setId(getRandomInteger(0, 1000));
builder.setName(getRandomString(20));
builder.setDepartment(Department.values()[getRandomInteger(0, Department.values().length - 1)]);
return builder;
}
public ImmutableEmployeeBuilder setId(int id) {
this.id = id;
return this;
}
public ImmutableEmployeeBuilder setName(String name) {
this.name = name;
return this;
}
public ImmutableEmployeeBuilder setDepartment(Department dept) {
this.department = dept;
return this;
}
public ImmutableEmployee build() {
return new ImmutableEmployee(id, name, department);
}
}
This is very useful when we have hard to construct objects, or we need to change fields that are final.
An here its the final result:
Without builders:
@Test
public void changeRoleTestWithoutBuilders() {
// building the initial state
Employee employee = new Employee();
employee.setId(1);
employee.setDepartment(Department.DEVELOPEMENT);
employee.setName("John Johnny");
// testing the SUT
EmployeeManager employeeManager = new EmployeeManager();
employeeManager.changeRole(employee, Department.MANAGEMENT);
// building the expectations
Employee expectedEmployee = new Employee();
expectedEmployee.setId(employee.getId());
expectedEmployee.setDepartment(Department.MANAGEMENT);
expectedEmployee.setName(employee.getName());
// assertions
assertThat(employee, is(expectedEmployee));
}
With builders:
@Test
public void changeRoleTestWithBuilders() {
// building the initial state
Employee employee = EmployeeBuilder.defaultValues().setId(1).setName("John Johnny").setDepartment(Department.DEVELOPEMENT).build();
// building the expectations
Employee expectedEmployee = EmployeeBuilder.clone(employee).setDepartment(Department.MANAGEMENT).build();
// testing the SUT
EmployeeManager employeeManager = new EmployeeManager();
employeeManager.changeRole(employee, Department.MANAGEMENT);
// assertions
assertThat(employee, is(expectedEmployee));
}
As you can see, the size of the test is much smaller, and the construction of objects became much simpler (and nicer if you have a better code format). The difference is greater if you have a more complex domain object (which is more likely in real-life applications and especially in legacy code).
Have fun!
Meta: this post is part of the Java Advent Calendar and is licensed under the Creative Commons 3.0 Attribution license. If you like it, please spread the word by sharing, tweeting, FB, G+ and so on! Want to write for the blog? We are looking for contributors to fill all 24 slot and would love to have your contribution! Contact Attila Balazs to contribute!