Easy testing with ObjectMothers and EasyRandom

Random dice

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.

6 thoughts on “Easy testing with ObjectMothers and EasyRandom

  1. Hmm I’ve used jFairy previously but this sounds heaps better. Sounds like it might require a lot less manual intervention…

    1. I think jFairy is a bit more specifically focused on a certain type of data (Customers, Companies, Dates, CreditCards, etc), while EasyRandom is more generic data. I guess it depends on your usecase.

  2. I like ObjectMothers…   my concern with EasyRandom is repeatable tests…      situation is:

      1) Main build fails because data just happened to be randomly generated with some invalid combination that does not work

      2) Try to repeat the test, but now always passing with random data not hitting that scenario

      3) Now I’m in this position of sometimes the tests work and sometimes it does not.   Do I action each failure or just start racking it up to random data problems and ignore it

      4) Now I’m starting to ignore tests due to random noise as tests increase…   and I’m in trouble of turning test errors into log messages (ignoring them as they become noise)

     

    Plus flip side, is learning about the data is important in understanding the solution and handling it.   Random data just says create the model and not worry about the specifics of the data.  Potential for becoming lazily dangerous about different data scenarios.   Ok, in machine learning we can run lots of combinations.   However, taking TDD approach, I’m hoping test writers are thinking heavily about the data rather than relying of randomisation

    My thoughts.   Nice article

  3. This looks quite useful and i want to try it out in my next project. How would i go about limiting the lengt of each field with this? For example a Person class whose first name can’t be longer than 40 chars and last name can’ be longer than 80 chars?

    1. Hi Dan-Mikkel, thanks for your response. There are a few options you can use. One option you have is when you’re using Hibernate Validator, for example, @Length(min=10, max=40), EasyRandom takes those Annotations into account. If you’re not using Hibernate Validator, you can customise the behaviour of EasyRandom to, for example, randomise the String length:

      EasyRandomParameters parameters = new EasyRandomParameters()
      .seed(123L)
      .stringLengthRange(5, 50);

      EasyRandom easyRandom = new EasyRandom(parameters);

      You can even go as far as customising the random behaviour for each field, but it requires a bit more configuration. You can find some examples here: https://github.com/j-easy/easy-random

Leave a Reply

Your email address will not be published. Required fields are marked *