Unit testing with Instancio

Estafet consultants occasionally produce short, practical tech notes designed to help the broader software development community and colleagues. 

If you would like to have a more detailed discussion about any of these areas and/or how Estafet can help your organization and teams with best practices in improving your SDLC, you are very welcome to contact us at enquiries@estafet.com 

Introduction

Instancio is a Java library for creating objects with dynamic random values, mainly used in unit tests. The main goal is to ease the development of unit tests, increase developer productivity, and also to introduce a slightly different approach to unit testing. By using randomly generated data for each run, we are able to cover different types of scenarios that we would’ve otherwise missed with manually created tests.

You know how developers sometimes tend to write unit tests in a way that they “know it will pass”? This is mostly unintentional, as the developers are writing tests to a “unit of code” they had just written, subconsciously forcing themselves to write happy paths. With Instancio, we can guarantee that such bias does not influence at least the POJOs used in the tests.

At this point, you are probably wondering, what will happen if a test with random data fails in the CI (or in a regular local build)? Can we reproduce that scenario locally/manually? Can we force Instancio to regenerate the same data? If this sounds like a wild, unstable, and messy approach to unit testing, I encourage you to keep reading!

Instancio has our backs, using “seeds” to generate the data, that we can use to reproduce the same scenario.

To summarise, by the end of this document we will be able to:

  1. Populate POJOs for unit tests in a smooth and easy manner
  2. Configure test data generation logic
  3. Analyse and reproduce failed dynamic tests
  4. Introduce a whole new layer of unit testing to catch these bugs early in the development process.

Setup

Instancio is packaged as a multi-release JAR and can be used with Java 8 or higher.

We will use maven for these examples. For a deeper dive into the configuration possibilities, refer to: https://www.instancio.org/user-guide/, we will cover only a few basic examples. 

The example project consists of a few models with a service, that interacts with them and saves them to a simulated database (a simple HashMap in-memory storage). The project can be found here.

If you have JUnit 5 on the classpath, then use instancio-junit. It includes a transitive dependency on instancio-core:

We will be using JUnit5. Your POM should be similar to this:

Usage

Familiarise yourself with the models that we will be using, here.

This is how an example test initially looks like:

We will now extend it with generation logic while going through it!

You can find the final test here.

We will now cover generation configuration methods, that typically take as an argument:

(<a selector, that will match the fields we want to affect>, <the value/rules we want to apply to the selection match(es)>)

Using set()

The set() method is the simplest to use, it specifies the exact rules for a field during generation. For example:

The resulting object looks like this:

Every time you run this snippet (by running the whole test again for example), a new Student object with randomly populated fields will be created, with the exception of the two fields we strictly specified the values of: countryCode and dateOfBirth

These modifiers allow us to have some control over the generation, whenever it makes sense (for example the date of birth cannot be 2000 years back, it’s illogical in our scenario).

Using generate()

The generate method allows us to specify generation rules, unlike the previous example where we explicitly hardcoded the values of fields. Let’s enhance the generator with generation rules, we are currently working with:

In our student object, we have a Phone object that has a ‘number’ field. Let’s use a generator that produces only numbers, and let’s give that a pattern to follow:

This change now produces the following object:

Notice the new phone number, now it looks better suited to our testing purposes and scenarios.

With the generate method we can adjust more interesting rules like:

  • Allow nullables
  • Allow empty
  • Specify collection sizes

For example, let’s set a minimum length rule for the email:

Using supply()

Supply has two variants, it uses either a generator (functional interface) for random values or a supplier for non-random values.

Non-random supplier example

Let’s change the date generation to:

Unlike in our previous example using set, supply() will provide a new unique instance to each field it supplies. Consider this snippet:

Random supplier example

We can use the second variant of supply() to provide random data for objects.

This method takes a Generator as an argument, which is a functional interface with the following signature:

Using the provided Random instance ensures that generated objects are reproducible. Since Generator is a functional interface it can be specified as a lambda expression. Let’s add Address object generation:

(We will start working with LocalDate instead of Date)

We can now see that our generation’s condition is satisfied:

Generators are powerful and can be used to generate complex objects, you can find more information here.

Ignoring Fields or Classes

By default, Instancio will attempt to populate every non-static field value. The ignore method can be used where this is not desirable. Let’s assume the email field is optional and we want to leave it out of the generation:

This will leave us with a null value for the email:

This behavior might be desirable in some test cases.

One important thing to note is that ignore() has precedence over other methods!

This means that if we try to generate for an ignored field, the field will be null:

In newer versions, this will not even compile:

If you need nullability in your generation, one thing you can do is explicitly configure it with withNullable() method:

Which will now yield null for some fields:

Using models

Another useful feature of Instancio is the ability to create object generation templates that you can reuse in multiple tests as a base. For example:

This method will now create a template model of our student, that has common rules that we don’t want to duplicate, and that we want to build upon. 

Example usage in our case:

This approach can make the tests readable and far more customizable.

Reproducing Failed Tests

Since using Instancio validates your code against random inputs on each test run, having the ability to reproduce failed tests with previously generated data becomes a necessity. Instancio supports this use case by reporting the seed value of a failed test in the failure message using JUnit’s publishReportEntry mechanism.

By adding this extension annotation to the class:

We can now observe in the logs the seed that we failed with:

We can now use this seed to reproduce the same exact test with the same exact objects:

Allowing us to debug further why the test failed!

Conclusion

In this document we managed to cover key features of the Instancio library, and why it should become a vital part of a developer’s toolkit! 

Now we know how to:

  1. Populate POJOs for unit tests in a smooth and easy manner
  2. Configure test data generation logic
  3. Reproduce failed tests
  4. Introduce a whole new layer of unit testing to catch these bugs early in the development process.

By Antonio Lyubchev, Consultant at Estafet

Stay Informed with Our Newsletter!

Get the latest news, exclusive articles, and updates delivered to your inbox.