When you use EasyRandom, You'd have written following code for each test:
EasyRandom easyRandom = new EasyRandom();
... = easyRandom.nextObject(...);
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();
}
into this:
@Test
@ExtendWith(EasyRandomExtension.class)
public void testSortAlgorithm(@EasyRandomed int[] ints) {
// When
int[] sortedInts = myAwesomeSortAlgo.sort(ints);
// Then
assertThat(sortedInts).isSorted();
}
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();
}
}
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 {}
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);
}
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);
}
}
}
}
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();
}
}
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
);
}
}
...
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);
}
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);
}
}
}
}
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)