Improving Unit Test Maintainability
Creating unit tests could be painful sometimes. Let's see how to improve the maintainability of unit tests with EasyRandom.
Join the DZone community and get the full member experience.
Join For FreeWhen doing unit tests, you have probably found yourself in the situation of having to create objects over and over again. To do this, you must call the class constructor with the corresponding parameters. So far, nothing unusual, but most probably, there have been times when the values of some of these fields were irrelevant for testing or when you had to create nested "dummy" objects simply because they were mandatory in the constructor.
All this has probably generated some frustration at some point and made you question whether you were doing it right or not; if that is really the way to do unit tests, then it would not be worth the effort.
That is to say, typically, a test must have a clear objective. Therefore, it is expected that within the SUT (system under test) there are fields that really are the object of the test and, on the other hand, others are irrelevant.
Let's take an example. Let's suppose that we have the class "Person"
with the fields Name
, Email
, and Age
. On the other hand, we want to do the unit tests of a service that, receiving a Person
object, tells us if this one can travel for free by bus or not. We know that this calculation only depends on the age. Children under 14 years old travel for free. Therefore, in this case, the Name
and Email
fields are irrelevant.
In this example, creating Person
objects would not involve too much effort, but let's suppose that the fields of the Person
class grow or nested objects start appearing: Address
, Relatives
(List of People
), Phone List
, etc. Now, there are several issues to consider:
- It is more laborious to create the objects.
- What happens when the constructor or the fields of the class change?
- When there are lists of objects, how many objects should I create?
- What values should I assign to the fields that do not influence the test?
- Is it good if the values are always the same, without any variability?
Two well-known design patterns are usually used to solve this situation: Object Mother and Builder. In both cases, the idea is to have "helpers" that facilitate the creation of objects with the characteristics we need.
Both approaches are widespread, are adequate, and favor the maintainability of the tests. However, they still do not resolve some issues:
- When changing the constructors, the code will stop compiling even if they are fields that do not affect the tests.
- When new fields appear, we must update the code that generates the objects for testing.
- Generating nested objects is still laborious.
- Mandatory and unused fields are hard coded and assigned by default, so the tests have no variability.
One of the Java libraries that can solve these problems is "EasyRandom." Next, we will see details of its operation.
What is EasyRandom?
EasyRandom is a Java library that facilitates the generation of random data for unit and integration testing. The idea behind EasyRandom is to provide a simple way to create objects with random values that can be used in tests. Instead of manually defining values for each class attribute in each test, EasyRandom automates this process, automatically generating random data for each attribute.
This library handles primitive data types, custom classes, collections, and other types of objects. It can also be configured to respect specific rules and data generation restrictions, making it quite flexible.
Here is a basic example of how EasyRandom can be used to generate a random object:
public class EasyRandomExample {
public static void main(String[] args) {
EasyRandom easyRandom = new EasyRandom();
Person randomPerson = easyRandom.nextObject(Person.class);
System.out.println(randomPerson);
}
}
In this example, Person
is a dummy class, and easyRandom.nextObject(Person.class)
generates an instance of Person
with random values for its attributes.
As can be seen, the generation of these objects does not depend on the class constructor, so the test code will continue to compile, even if there are changes in the SUT. This would solve one of the biggest problems in maintaining an automatic test suite.
Why Is It Interesting?
Using the EasyRandom library for testing your applications has several advantages:
- Simplified random data generation: It automates generating random data for your objects, saving you from writing repetitive code for each test.
- Facilitates unit and integration testing: By automatically generating test objects, you can focus on testing the code's behavior instead of worrying about manually creating test data.
- Data customization: Although it generates random data by default, EasyRandom also allows you to customize certain fields or attributes if necessary, allowing you to adjust the generation according to your needs.
- Reduced human error: Manual generation of test data can lead to errors, especially when dealing with many fields and combinations. EasyRandom helps minimize human errors by generating consistent random data.
- Simplified maintenance: If your class requirements change (new fields, types, etc.), you do not need to manually update your test data, as EasyRandom will generate them automatically.
- Improved readability: Using EasyRandom makes your tests cleaner and more readable since you do not need to define test values explicitly in each case.
- Faster test development: By reducing the time spent creating test objects, you can develop tests faster and more effectively.
- Ease of use: Adding this library to our Java projects is practically immediate, and it is extremely easy to use.
Where Can You Apply It?
This library will allow us to simplify the creation of objects for our unit tests, but it can also be of great help when we need to generate a set of test data. This can be achieved by using the DTOs of our application and generating random objects to later dump them into a database or file. Where it is not recommended: this library may not be worthwhile in projects where object generation is not complex or where we need precise control over all the fields of the objects involved in the test.
How To Use EasyRandom
Let's see EasyRandom in action with a real example, environment used, and prerequisites.
Prerequisites
- Java 8+
- Maven or Gradle
Initial Setup
Inside our project, we must add a new dependency. The pom.xml file would look like this:
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-random-core</artifactId>
<version>5.0.0</version>
</dependency>
Basic Use Case
The most basic use case has already been seen before. In this example, values are assigned to the fields of the person class in a completely random way. Obviously, when testing, we will need to have control over some specific fields. Let's see this as an example. Recall that EasyRandom can also be used with primitive types. Therefore, our example could look like this.
public class PersonServiceTest {
private final EasyRandom easyRandom = new EasyRandom();
private final PersonService personService = new PersonService();
@Test
public void testIsAdult() {
Person adultPerson = easyRandom.nextObject(Person.class);
adultPerson.setAge(18 + easyRandom.nextInt(80));
assertTrue(personService.isAdult(adultPerson));
}
@Test
public void testIsNotAdult() {
Person minorPerson = easyRandom.nextObject(Person.class);
minorPerson.setAge(easyRandom.nextInt(17));
assertFalse(personService.isAdult(minorPerson));
}
}
As we can see, this way of generating test objects protects us from changes in the "Person
" class and allows us to focus only on the field we are interested in.
We can also use this library to generate lists of random objects.
@Test
void generateObjectsList() {
EasyRandom generator = new EasyRandom();
//Generamos una lista de 5 Personas
List<Person> persons = generator.objects(Person.class, 5)
.collect(Collectors.toList());
assertEquals(5, persons.size());
}
This test, in itself, is not very useful. It is simply to demonstrate the ability to generate lists, which could be used to dump data into a database.
Generation of Parameterized Data
Let's see now how to use this library to have more precise control in generating the object itself. This can be done by parameterization.
Set the value of a field. Let's imagine the case that for our tests, we want to keep certain values constant (an ID, a name, an address, etc.) To achieve this, we would have to configure the initialization of objects using "EasyRandomParameters
" and locate the parameters by their name.
Let's see how:
EasyRandomParameters params = new EasyRandomParameters();
// Asignar un valor al campo por medio de una función lamba
params.randomize(named("age"),()-> 5);
EasyRandom easyRandom = new EasyRandom(params);
// El objeto tendrá siempre una edad de 5
Person person = easyRandom.nextObject(Person.class);
Of course, the same could be done with collections or complex objects.
Let's suppose that our class Person
, contains an Address
class inside and that, in addition, we want to generate a list of two persons.
Let's see a more complete example:
EasyRandomParameters parameters = new EasyRandomParameters()
.randomize(Address.class, () -> new Address("Random St.", "Random City"))
EasyRandom easyRandom = new EasyRandom(parameters);
return Arrays.asList(
easyRandom.nextObject(Person.class),
easyRandom.nextObject(Person.class)
);
Suppose now that a person can have several addresses. This would mean the "Address
" field will be a list inside the "Person
" class.
With this library, we can also make our collections have a variable size. This is something that we can also do using parameters.
EasyRandomParameters parameters = new EasyRandomParameters()
.randomize(Address.class, () -> new Address("Random St.", "Random City"))
.collectionSizeRange(2, 10);
EasyRandom easyRandom = new EasyRandom(parameters);
// El objeto tendrá una lista de entre 2 y 10 direcciones
Person person = easyRandom.nextObject(Person.class);
Setting Pseudo-Random Fields
As we have seen, setting values is quite simple and straightforward. But what if we want to control the randomness of the data? We want to generate random names of people, but still names and not just strings of unconnected characters. This same need is perhaps clearer when we are interested in having randomness in fields such as email, phone number, ID number, card number, city name, etc.
For this purpose, it is useful to use other data generation libraries. One of the best-known is Faker
.
Combining both libraries, we could get a code like this:
EasyRandomParameters params = new EasyRandomParameters();
//Generar número entre 0 y 17
params.randomize(named("age"), () -> Faker.instance().number().numberBetween(0, 17));
// Generar nombre "reales" aleatorios
params.randomize(named("name"), () -> Faker.instance().name().fullName());
EasyRandom easyRandom = new EasyRandom(params);
Person person = easyRandom.nextObject(Person.class);
There are a multitude of parameters that allow us to control the generation of objects.
Closing
EasyRandom is a library that should be part of your backpack if you develop unit tests, as it helps maintain unit tests. In addition, and although it may seem strange, establishing some controlled randomness in tests may not be a bad thing. In a way, it is a way to generate new test cases automatically and will increase the probability of finding bugs in code.
Opinions expressed by DZone contributors are their own.
Comments