Easy testing with ObjectMothers and EasyRandom

Imagine you’re a developer and you have to do some unit testing. Not hard to image, I think. And imagine that as part of your tests, you need to have a bit of setup data. And your next test, well, it needs similar data, but just a slight variation of it and a pattern emerges. Now, what is a good way to manage this test data and make it easier to maintain this code? This blogpost will dive into a few solutions and hopefully make testing slightly easier.

Test data duplication

A common pattern I frequently see when developing tests is as described above. There are few test methods with some setup data, some actions, and then some verifications. The test data is created as part of the test, and the next test is a copy and paste of the first test. Sometimes there’s a slight variation in the setup, maybe to make the test go into the a different path.

Even though this code is just test code, you still have to maintain it. Also, there is a cost involved with maintaining this test data. When things change then you might need to change test data in numerous locations. This is making maintaining these tests harder than is needed. Another issue which often arises is that it’s hard to see what the variation in the test data is. Let’s give an example:

@Test
fun placeOrderWithoutProductShouldError() {
    val customer = Customer("Peter", emptyList())

    assertThrows<IllegalArgumentException> {  orderService.placeOrder(customer) }
    verify(orderRepository, never()).save(customer)
}

@Test
fun placeOrderWithOneProductShouldBeOk() {
    val product = Product("AB101", "Product 1", "The first product")
    val order = Order(product, 10)
    val customer = Customer("Peter", listOf(order))

    orderService.placeOrder(customer)
    verify(orderRepository).save(customer)
}

@Test
fun placeOrderWithHighQuantityShouldError() {
    val product = Product("AB101", "Product 1", "The first product")
    val order = Order(product, 10000)
    val customer = Customer("Peter", listOf(order))

    assertThrows<IllegalArgumentException> {  orderService.placeOrder(customer) }
    verify(orderRepository, never()).save(customer)
}

As you can see in the example above, the methods are very similar. There’s a slight variation in the test setup code. When a codebase grows for a period of time, code like this becomes often harder to maintain. And there is a solution to deal with this: let’s meet Object Mothers.

What’s an Object Mother?

As Martin Fowler describes: “An object mother is a kind of class used in testing to help create example objects that you use for testing.”. So, an Object Mother is test data factory with a slightly more interesting name. A basic example of an Object Mother can be seen below:

fun createValidCustomer(): Customer {
   // create valid customer here
}

What is important here is not the implementation, but a way to create a valid customer.

Martin Fowler describes the following in his blogpost:

“Object Mothers do have their faults. In particular there’s a heavy coupling in that many tests will depend on the exact data in the mothers. “

In regard to this, I have the following to say:

Don’t depend on the actual values! It will make your tests needlessly coupled and brittle.

We’ll dive more into this later, but this is very important: don’t depend on the values of the object mothers. The only thing the object mothers should be used for is setting an object into a certain state as indicated by the method name. So, an object mother can put an object into a valid or invalid state. For example, a customer with all mandatory fields sets or with fields missing. Don’t depend on the actual values of the fields.

If you do need the actual values of the object manage them in your test class instead. So, if the name of a customer is important, do something like the following:

val customer = createValidCustomer()
customer.name = "Rick"

This way you don’t have to manage all the fields of the customer. The only thing you have to manage are the fields of the object which are relevant for the test. All the other fields can be safely assumed to be valid. As a result, in the test class you know that you are working with a valid customer. This approach greatly simplifies the management of test data. In our team we’ve used this approach in almost every test case, even when we only need to create a simple object. This standard approach makes managing test data incredibly easy, since there is almost always an ObjectMother available with different variations of the object you need.

Implementing an Object Mother

There are a few ways to implement an object mother. An easy way to get started is just to hardcode the implementation, something like this:

fun createValidCustomer(): Customer {
    val product = Product("AB101", "Product 1", "The first product")
    val order = Order(product, 10)
    return Customer("Peter", mutableListOf(order))
}

fun createInvalidCustomer(): Customer {
    return Customer("Peter", mutableListOf())
}

While this is a great way to get started, managing test data this way still gets a bit tedious. This is especially true when dealing with classes which have many properties or relationships. Another downside is that is that using hardcoded values this way might tempt the user of this ObjectMother to assert on the hardcoded values in the test. This is not what we want, since this creates unnecessary dependencies between the tests.

For these reasons, we’re going to introduce a helper library, called Easy Random (formerly know as EnhancedRandom).

What is Easy Random

Easy Random  is a library for generating test data. Easy Random is quite smart; it takes into account the validation rules defined using JSR-303 / Bean Validation 1.1 / Bean Validation 2.0 (since 3.8.0), and it creates instances of deeply nested classes. While providing a great out of the box experience, it also provides an extensive way of customising. And since Easy Random creates values at random, it’s not possible to depend on the hardcoded values, which is a great safeguard in protecting yourself from depending on the test values.

How to use Easy Random

Using Easy Random is easy:

val easyRandom = EasyRandom()
val customer = easyRandom.nextObject(Customer::class.java)

Or, in Java:

EasyRandom easyRandom = new EasyRandom();
Customer customer = easyRandom.nextObject(Customer.class);

This would generate a new customer with random values. For example, name of the customer would be a string such as “GfK3MFRmF”. The customers age would be an age in the whole range of Integers. This means an age of 43898439 could be generated, but so could an age of -123903.

We want to have a bit more control over the generation of data so that it doesn’t conflict with our validation. We also want to create more realistic data, and make it possible to customise the generation of data. For example, to limit the number of possible orders created, you can use the following code:

val parameters = new EasyRandomParameters()
    .collectionSizeRange(1, 2)
val random = EasyRandom(parameters)
val customer = random.nextObject(Customer::class.java)

There are plenty more options possible, as demonstrated from one of the documentation examples:

EasyRandomParameters parameters = new EasyRandomParameters()
   .seed(123L)
   .objectPoolSize(100)
   .randomizationDepth(3)
   .charset(forName("UTF-8"))
   .timeRange(nine, five)
   .dateRange(today, tomorrow)
   .stringLengthRange(5, 50)
   .collectionSizeRange(1, 10)
   .scanClasspathForConcreteTypes(true)
   .overrideDefaultInitialization(false)
   .ignoreRandomizationErrors(true);

EasyRandom easyRandom = new EasyRandom(parameters);

Conclusion

Using EasyRandom in combination with ObjectMothers is a great way to improve your testing. When used consistently in the codebase, it will lower the maintenance on the unit test and will make writing tests easier and more pleasant.

Older Post
Newer Post

Leave a Reply

Your email address will not be published.