“Much unhappiness has come into the world because of bewilderment and things left unsaid” - Fyodor Dostoyevsky

Random Data Generators and Builders

This article isn't really about Fyodor as such, it's about how a combination of random data generation with a builder pattern can improve your testing. The big benefit to using a library like Fyodor to produce the random data is that when you do get a failure due to some edge-case you are able to reproduce it quickly and easily and (hopefully) fix it.

Setting up fixtures - the objects and data you're testing against - for your tests can be difficult, time-consuming and error-prone especially in large Enterprise, legacy or unfamiliar (or all three!) systems. Once we start writing integration style tests where our objects may be persisted, (de)serialized or sent over the wire then things can really start to become challenging and frustrating.

For Example...

Let’s imagine a simple car that we want to create for a test:

public class Car {
    String name;
    String description;
    String productCode;
    Manufacturer manufacturer;
    LocalDate dateOfManufacture;
    BigDecimal price;
    Engine engine;
}

And some typical test code to create one:

public class SomeTest {
    private final LocalDate DATE_OF_MANUFACTURE = new LocalDate().minusYears(10);
    private Car car;

    @Before
    public void setUpCar() {
        car = new Car();
        car.setDateOfManufacture(DATE_OF_MANUFACTURE);
    }
}

Our test's only interested in the age of our car but already some questions spring to mind about the rest of it:

(And on top of all this we might not even care about the car anyway, we might just need an old car to set something else up)

The Builder

Using Builders to create our fixtures can address some of these issues:

public class CarBuilder {
    String name = "SuperBadDog";
    String description = "Baddest Super Dog";
    String productCode = "HUFS-SH7E9BE-743-D";
    Manufacturer manufacturer = Manufacturer.TOYOTA;
    LocalDate dateOfManufacture = new LocalDate().minusYears(10);
    BigDecimal price = new BigDecimal(20000);
    Engine engine = EngineBuilder.engineBuilder().build();
    
    private CarBuilder(){}
    
    public static CarBuilder carBuilder() {
        return new CarBuilder();
    }
    
    public static CarBuilder expensiveCarBuilder() {
        CarBuilder builder = carBuilder();
        return builder.withPrice(100000);
    }
    
    public CarBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public CarBuilder withDescription(String description) {
        this.description = description;
        return this;
    }
     
    public CarBuilder withManufacturer(Manufacturer manufacturer) {
        this.manufacturer = manufacturer;
        return this;
    }
    
    public CarBuilder withDateOfManufacture(LocalDate date) {
        this.dateOfManufacture = date;
        return this;
    }
    
    public CarBuilder withAge(Integer age) {
        return withDateOfManufacture(new LocalDate().minusYears(age);
    }
    
    public CarBuilder withPrice(BigDecimal price) {
        this.price = price;
        return this;
    }
    
    public CarBuilder withPrice(Integer price) {
        return withPrice(new BigDecimal(price));
    }
    
    public CarBuilder withEngine(Engine engine) {
        this.engine = engine;
        return this;
    }
    
    public Car build(){
        Car car = new Car();
        car.setName(name);
        car.setDescription(description);
        car.setManufacturer(manufacturer);
        car.setDateOfManufacture(dateOfManufacture);
        car.setPrice(price);
        car.setEngine(engine);
        return car;
    }
}

This makes our test setup look a bit nicer and takes away some of the concerns we were having:

public class SomeTest {
    private final Integer AGE = 10;
    private Car car;

    @Before
    public void setUpCar(){
        car = CarBuilder.carBuilder().withAge(AGE).build();
    }
}

A nice feature of builders is that you can add and overload methods to make your fixture setup simpler, more readable and more meaningful:

Better Data

We’ve made our test setup a lot better with the builder but we can do better with the quality of the data. At the moment every test that uses our builder is going to get a Car with exactly the same default attributes which may create its own false-positive problem for us.

What we need is random data:

public class CarBuilder {
    private String name = RDG.string(15).next();
    private String description = RDG.string(50).next();
    private String productCode = RDG.productCode().next();
    private Manufacturer manufacturer = RDG.values(Manufacturers.values());
    private Date dateOfManufacture = RDG.localDate(aged(closed(years(3), years(15)))).next();
    private BigDecimal price = RDG.bigDecimal(50000).next();
    private Engine engine = EngineBuilder.engineBuilder().build();
    ...
}

Well this looks exciting! Let’s take a look at some of this RDG (RandomDataGenerator) class:

public class RDG {

    public static Generator<Integer> integer = integer(Integer.MAX_VALUE);

    public static Generator<Integer> integer(Integer max) {
        return new IntegerGenerator(max);
    }

    public static Generator<Integer> integer(Range<Integer> range) {
        return new IntegerGenerator(range);
    }

    public static Generator<String> string = string(30);

    public static Generator<String> string(Integer max) {
        return new StringGenerator(max);
    }
    ...
}

It’s basically a big collection of static methods returning Generator<T> objects. Generator<T>s are the core of Fyodor, a simple interface with one method next(), expecting implementing classes to be effectively a never-ending iterator of random values:

public interface Generator<T> {
    public T next();
}

Let's take a look at the IntegerGenerator:

class IntegerGenerator implements Generator<Integer> {

    private final Integer min;
    private final Integer max;

    IntegerGenerator(Integer max) {
        this.max = max;
        this.min = 0;
    }

    IntegerGenerator(Range<Integer> range) {
        this.min = range.lowerBound();
        this.max = range.upperBound();
    }

    @Override
    public Integer next() {
        return randomValues().randomInteger(min, max);
    }
}

Quite self-explanatory hopefully but notice the call to randomValues() in its next() implementation, this is Fyodor's internal way of managing the source of randomness for its generators. This gives it the ability to reproduce test failures or any other scenario by using a specific seed value - using random data to create more effective tests is all well and good but it can be frustrating to see intermittent failures and have no way to track them down.

Finally, notice in our builder that we're populating our product code with a call to RDG.productCode() to get a Generator<String>. The product code generator is actually a bespoke generator written for our Car's specially-formatted product code:

public class ProductCodeGenerator implements Generator<String> {

    private Generator<String> firstBit = RDG.string(5, CharacterSetFilter.LettersOnly);
    private Generator<String> secondBit = RDG.string(7, CharacterSetFilter.LettersAndDigits);
    private Generator<Integer> number = RDG.integer(Range.closed(100, 999));
    private Generator<String> lastBit = RDG.string(1, "DXZ");

    public String next() {
        return String.format("%s-%s-%d-%s",
                firstBit.next().toUpperCase(),
                secondBit.next().toUpperCase(),
                number.next(),
                lastBit.next());
    }
}

By extending Fyodor's core RDG class with our own we can seamlessly add bespoke generators specific to our domain.

The methods on the RDG class form Fyodor's public API for creating random data generators, check out the user guide for what it can do, how to use it and extending it to create your own generators.