DEV Community

Avinash Ananth Narayan R
Avinash Ananth Narayan R

Posted on

using EasyRandom with JUnit5

When you use EasyRandom, You'd have written following code for each test:

EasyRandom easyRandom = new EasyRandom();
... = easyRandom.nextObject(...);
Enter fullscreen mode Exit fullscreen mode

What if we remove that? Make this:

@Test
public void testSortAlgorithm() {

   // Given
   int[] ints = easyRandom.nextObject(int[].class);

   // When
   int[] sortedInts = myAwesomeSortAlgo.sort(ints);

   // Then
   assertThat(sortedInts).isSorted();

}
Enter fullscreen mode Exit fullscreen mode

into this:

@Test
@ExtendWith(EasyRandomExtension.class)
public void testSortAlgorithm(@EasyRandomed int[] ints) {
   // When
   int[] sortedInts = myAwesomeSortAlgo.sort(ints);

   // Then
   assertThat(sortedInts).isSorted();

}
Enter fullscreen mode Exit fullscreen mode

or this?

@ExtendWith(EasyRandomExtension.class)
class MyAwesomeSortAlgoTest {
    @EasyRandomed 
    private int[] ints;

    @Test
    public void testSortAlgorithm() {
       // When
       int[] sortedInts = myAwesomeSortAlgo.sort(ints);

       // Then
       assertThat(sortedInts).isSorted();

    }
}
Enter fullscreen mode Exit fullscreen mode

No more EasyRandom, the beans can be taken as params or as fields.

Note that there is already a JUnit5 extension for this which targets 3.9.0 of easy-random, at which point it was RandomBeans, but the EasyRandom has since changed group and artifact under which it identifies.

Here I'll outline how it can be easily implemented. At crux, we just need to have EasyRandomExtension to implement ParameterResolver to resolve parameter and BeforeEachCallback to resolve the marked by EasyRandomed annotation.

We'll start with the annotation:

@Documented @Target({PARAMETER, FIELD}) @Retention(RUNTIME)  
public @interface EasyRandomed {}
Enter fullscreen mode Exit fullscreen mode

I'll be covering the customization of the generated objects later

Implementation of EasyRandomExtension

1. Resolving annotated parameters

To work with ParameterResolver, we need to implement two methods supportsParameter and resolveParameter sounds reasonable enough:

@Override  
public boolean supportsParameter(
    ParameterContext parameterContext,
    ...
) throws ... {
    return parameterContext.getParameter()
        .getAnnotation(EasyRandomed.class) != null;
}

@Override  
public Object resolveParameter(
    ParameterContext parameterContext,
    ...
) throws ... {
    final Parameter parameter = parameterContext.getParameter();
    final Class<?> paramType = parameter.getType();
    return new EasyRandom().nextObject(paramType);
}
Enter fullscreen mode Exit fullscreen mode

2. Resolving annotated fields

the BeforeEachCallback gives a nice place to do some pre-processing on the test instances.

@Override  
public void beforeEach(ExtensionContext context) throws ... {
    List<Object> instances = context.getRequiredTestInstances()
        .getAllInstances();
    for (Object instance : instances) {
        for (Field field : instance.getClass().getFields()) {
            if (field.getAnnotation(EasyRandomed.class) != null) {
                final Class<?> fieldType = field.getType();
                final Object fieldValue = new EasyRandom()
                    .nextObject(fieldType);
                FieldUtils.writeField(field, instance, fieldValue, true);
             }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

FieldUtils is from Commons Lang3, which makes my life easier. I'm not going to discuss how a private field of class can be set. I'm not going down that rabbit hole (again!)

Case: I want to customize my object

Till now we didn't provide a way for the client to customize the easy random generation, so let us revisit the Annotation and make some tweaks:

@Documented @Target({PARAMETER}) @Retention(RUNTIME)  
public @interface EasyRandomed {
    Class<? extends Supplier<EasyRandom>> value() 
        default DefaultEasyRandomSupplier.class;
}

public class DefaultEasyRandomSupplier implements Supplier<EasyRandom> {
    @Override
    public EasyRandom get() {
        return new EasyRandom();
    }
}
Enter fullscreen mode Exit fullscreen mode

So if you want to customize, you can provide your own instance of EasyRandom for the use-case.

the above sorting example can be like this:

...
̥̥@Test
public void testSortAlgorithm(@EasyRandomed(BigArrayProvider.class) int[] ints) {
   // When
   int[] sortedInts = myAwesomeSortAlgo.sort(ints);

   // Then
   assertThat(sortedInts).isSorted();

}
public static class BigArrayProvider implements Supplier<EasyRandom> {
    @Override
    public EasyRandom get() {
        return new EasyRandom(
            new EasyRandomParameters()
                .collectionSizeRange(30000, 300000); // 30K to 300K ints
        );
    }
}
...
Enter fullscreen mode Exit fullscreen mode

Now, we just need to use the configured supplier to get the EasyRandom object in EasyRandomExtension, instead of generating them ourselves.
the only changes are in the resolveParameter and beforeEach methods.

resolveParameter becomes:

@Override  
public Object resolveParameter(
    ParameterContext parameterContext,
    ...
) throws ... {
    final Parameter parameter = parameterContext.getParameter();
    final Class<?> paramType = parameter.getType();
    final EasyRandomed ann = parameter.getAnnotation(EasyRandomed.class);
    return ann.value().newInstance().get().nextObject(paramType);
}
Enter fullscreen mode Exit fullscreen mode

similarly, beforeEach becomes:

@Override  
public void beforeEach(ExtensionContext context) throws ... {
    List<Object> instances = context.getRequiredTestInstances()
        .getAllInstances();
    for (Object instance : instances) {
        for (Field field : instance.getClass().getFields()) {
            if (field.getAnnotation(EasyRandomed.class) != null) {
                final Class<?> fieldType = field.getType();
                final EasyRandomed ann = 
                    field.getAnnotation(EasyRandomed.class);
                final Object fieldValue = ann.value().newInstance().get()
                    .nextObject(fieldType);
                FieldUtils.writeField(field, instance, fieldValue, true);
             }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So what is the status of this project? It's a single file, and we wouldn't see it getting merged into EasyRandom. Why? as EasyRandom is not intentended to be used with just JUnit5/4, it can be used in any other unit testing framework for JVM.

Explanations on some of the design choices

Q: Why didn't you just provide me with an API similar to RandomBeansExtension?
A: Sure we could've just over-engineered our extension to provide all the configurations of EasyRandomPrameter, but that would result in over-engineering. Also you can imagine what a nightmare it would be when a parameter from EasyRandomParameter is deprecated or a new one is introduced.
Let's be honest, I am not looking forward to work on this small piece of code for rest of eternity, I have other priorities, and it is easier for the project maintenance to keep it simple. Also this way, you can just wash your hands off of all the responsibility with the API of EasyRandom. I am only interested in using EasyRandom.nextObject(...) method.

Top comments (0)