Introduction
Lately I have been wondering what is the best way to write readable tests. The typical structure of a test is Given-When-That or Arrange-Act-Assert.
My focus went to the arranging code, since most of my tests looked similar to the following:
void sendPackage_givenPackageInWaitingForPostingState_sendsPackage() {
// given
Coordinates coordinates = new Coordinates();
coordinates.setLatitude(0);
coordinates.setLongitude(0);
Address address = new Address();
address.setCoordinates(coordinates);
Package sendingPckg1 = new Package();
sendingPckg1.setBarcode("S1");
sendingPckg1.setCreatedDate(LocalDateTime.of(2020, 1, 1, 0, 0));
sendingPckg1.setState(WAITING_FOR_POSTING);
sendingPckg1.setAddress(address);
packageStore.create(sendingPckg1);
// when
Package sentPackage = service.sendPackage(sendingPckg1);
// that
assertThat(sentPackage.getState()).isEqualTo(IN_TRANSIT)
}I didn’t particularly like the way I was defining the entities in each test as it took a lot of reading space. The reading flow was also interrupted when setting up nested dependencies such as Package has Address deliveryAddress, which has Coordinates coordinates and we first need to set up coordinates, then below that create the address to which we set the coordinates and then create the package to which we set the address.
The options I found are: constructor methods and factories, object mothers, builders
Constructor methods
Yes, the simplest way would be to create constructors in the entity itself. We could even use Lombok’s annotations like @AllArgsConstructor or @RequiredArgsConstructor. If we don’t want to do that, we can define constructor methods.
Or, we can instead create a constructor method in our test, which acts as a constructor for our entity. This allows us to specify the necessary fields in the test while hiding the unimportant, but still required fields.
void sendPackage_givenPackageInWaitingForPostingState_sendsPackage() {
// given
Package sendingPckg1 = createPackage(WAITING_FOR_POSTING)
// when
Package sentPackage = service.sendPackage(sendingPckg1);
// that
assertThat(sentPackage.getState()).isEqualTo(IN_TRANSIT)
}
Package createPackage(PackageState state) {
Coordinates coordinates = new Coordinates();
coordinates.setLatitude(0);
coordinates.setLongitude(0);
Address address = new Address();
address.setCoordinates(coordinates);
Package pckg = new Package();
pckg.setBarcode("S1");
pckg.setCreatedDate(LocalDateTime.of(2020, 1, 1, 0, 0));
pckg.setState(WAITING_FOR_POSTING);
pckg.setAddress(address);
return packageStore.create(pckg);
}
This approach creates a really nice and readable tests. There is just one caveat, after using this approach for multiple tests we end up with many methods with the same erasure. ex. createUser(String username) and createUser(String password) so we create createUserWithUsername(String username) and createUserWithPassword(String password), which for complex methods becomes a mess.
These methods we can then extract into Test Factories so we can just call PackageFactory.createPackage(WAITING_FOR_POSTING)
Object Mother
Object Mother pattern is used to define personas in your tests. Instead of always creating a new user for each test we can create a user John, that is a clerk in the package shipping company and has certain roles, authorities and use cases he needs to do.
This means that instead of having methods
PackageFactory.createPackage(...)
PackageFactory.createPackage(...)
PackageFactory.createPackage(...)we call
PacakgeFactory.createLegoPackage()
PackageFactory.createAmazonPackage()
PackageFactory.createHomeDeliveryPackage()I like this approach as it defines personas in our system which have their responsibilities clearly stated.
Builders
Another approach is to use the Builder pattern to construct our objects. I like that builders use the fluent methods so that we can inline these setters. I think that it becomes more readable, for example:
Package createPackage(PackageState state) {
Package pckg = Package.Builder()
.barcode("S1");
.createdDate(LocalDateTime.of(2020, 1, 1, 0, 0));
.state(WAITING_FOR_POSTING);
.address(Address.Builder()
.coordinates(Coordinates.Builder()
.latitude(0)
.longitude(0).build())
.build())
.build();
return packageStore.create(pckg);
}The easiest way to make a builder would be to use lomboks @Builder annotation. However, I recently came an approach, where we write the builder manually, but we can also add our test specific logic into it. This gives us much more freedom.
Lets start with the Builder interface.
interface Builder
/**
* Represents an instance builder.
*
* @param <T> type of instance to be built
* @param <PB> type of parent builder
*/
public interface Builder<T, PB extends Builder<?, ?>> {
/**
* Returns object instance being built
*/
T getInstance();
/**
* Returns the setter function for up-tranversal
*/
Function<T, PB> getSetter();
/**
* Builds the instance and traverses up in builder chain.
*/
default PB build() {
Function<T, PB> setter = getSetter();
notNull(setter, () -> new BuilderException("'setter' is not set"));
return setter.apply(buildPlain());
}
/**
* Return builded instance with configured properties.
*/
default T buildPlain() {
return getInstance();
}
/**
* Build builded instance with given mapper (i.e. to create an entity view from builded instance using static methods)
*
* @param <R> type of resultant object
*/
default <R> R buildWith(Function<T, R> mapper) {
return mapper.apply(getInstance());
}
}with this interface we can create a builder for each entity.
@Getter
@NoArgsConstructor
@AllArgsConsructor
public class CoordinatesBuilder<PB extends Builder<?, ?>> implements Builder<Coordinates, PB> {
private Function<Coordinates, PB> setter;
private final Coordinates instance = new Coordinates();
public CoordinatesBuilder<PB> latitude(int latitude) {
instance.setLatitude(latitude);
return this;
}
// ...
}to use the builder we can use it as follows:
Coordinates coordinates = new CoordinatesBuilder<>().latitude(0).longitude(0).buildPlain();Building children
but wait, isn’t that the same as the lomboks @Builder you may ask? Well yes, it is a builder after all, but a Builder with up-traversal.
This means we have more fluent approach when building child depdendencies. Lets look at AddressBuilder:
public class AddressBuilder<PB extends Builder<?, ?>> implements Builder<Address, PB> {
private Function<Address, PB> setter;
private final Address instance = new Address();
public AddressBuilder<PB> coordinates(Coordinates coordinates) {
instance.setCoordinates(coordinates);
return this;
}
public CoordinatesBuilder<AddressBuilder<PB>> coordinates() {
return new CoordinatesBuilder<>(this::coordinates);
}
// ...
}
Aha, up-traversal. we can use our fluent syntax to use .coordinates() which returns a new builder. Then instaed of .buildPlain() we use .build() to return up to the parent builder.
Address address = new AddressBuilder()
.coordinates()
.latitude(0)
.longitude(0)
.build()
.buildPlain();This approach is in my opinion very elegant. We can use this to construct the child entity, we can use this to construct collections, we can add custom methods, and so much more.
Building collections of children
Now lets imagine the Package entity has multiple items inside, lets say @OneToMany List<Item> items, we can introduce three methods into our builder.
public class PackageBuilder<PB extends Builder<?, ?>> implements Builder<Package, PB> {
// ...
public PackageBuilder<PB> items(List<Item> items) {
instance.setItems(items);
return this;
}
public PackageBuilder<PB> item(Item item) {
instance.getItems().add(item);
return this;
}
public ItemBuilder<PackageBuilder<PB>> item() {
return new ItemBuilder<>(this::item);
}
// ...
}
With this approach we can set the whole list of items, or we can build each item individually.
Persistance
We can also add new builder abstractions such as EntityBuilder which would have a .persist() method. This could improve our builders even more since we could also perist our dependencies after building them.
For this we need to get access to spring components outside of spring, so that we can inject the repository into our builder.
/**
* Singleton for accessing Spring application context from non-spring context.
*/
@Service
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext ctx;
@Override
public void setApplicationContext(@NotNull ApplicationContext context) throws BeansException {
ctx = context;
}
public static ApplicationContext getApplicationContext() {
return ctx;
}
}The ApplicationContextUtils uses a Service locator pattern. It sets up a static application context that we can access from our builder.
/**
* Represents an entity instance builder.
*
* @param <T> type of instance to be built
* @param <BT> type of this builder
* @param <PB> type of parent builder
*/
public abstract class EntityBuilder<T extends DomainObject<T>, BT extends EntityBuilder<T, BT, PB>, PB extends Builder<?, ?>> extends Builder<T, PB> {
public BT persist() {
var repository = getRepository();
repository.update(getInstance());
return this;
}
public JpaRepository<T, ?> getRepository() {
ApplicationContext applicationContext = ApplicationContextUtils.getApplicationContext();
String[] beanNames = applicationContext.getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference<JpaRepository<T, ?>>() {}));
notEmpty(beanNames, () -> new BuilderException("repository not found"));
return (JpaRepository<T, ?>) applicationContext.getBean(beanNames[0]);
}
}Now we can use this entity to persist our build entites as well:
// ...
public class CoordinatesBuilder<PB extends Builder<?, ?>> extends EntityBuilder<Coordinates, CoordinatesBuilder<PB>, PB> {
// ...
}and we can then call
Coordinates coordinates = new CoordinatesBuilder<>().latitude(0).longitude(0).persist().buildPlain();