TestUtils.java
package it.fulminazzo.yagl;
import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.fulmicollection.utils.ExceptionUtils;
import it.fulminazzo.fulmicollection.utils.ReflectionUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mockito.ArgumentCaptor;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.internal.progress.MockingProgress;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
/**
* The type Test utils.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class TestUtils {
/**
* Allows testing all the given <i>executor</i> methods that match the <i>methodFinder</i> predicate.
* Each one of them is first invoked using {@link #testSingleMethod(Object, Method, Object[], Object, String, Class[])}.
* Then, it uses <i>captorsValidator</i> to validate the passed parameters.
*
* @param executor the executor
* @param methodFinder the method finder
* @param captorsValidator the captors validator
* @param staticObjects the objects that will be used for the creation of the parameters of <i>targetMethod</i>. If the required class is present among these objects, then the one provided will be used. Otherwise, {@link #mockParameter(Class)} will be called.
* @param target the target
* @param invokedMethod the invoked method
* @param invokedMethodParamTypes the type of the parameters when invoking <i>invokedMethod</i>. These will also be the types of the returned captors
*/
public static void testMultipleMethods(final @NotNull Object executor, final @NotNull Predicate<Method> methodFinder,
final @NotNull Consumer<ArgumentCaptor<?>[]> captorsValidator,
final Object @NotNull [] staticObjects,
final @NotNull Object target, final String invokedMethod,
final Class<?> @NotNull ... invokedMethodParamTypes) {
@NotNull List<Method> methods = new Refl<>(executor).getMethods(methodFinder);
if (methods.isEmpty()) throw new IllegalArgumentException("Could not find any method matching the given arguments.");
for (final Method method : methods) {
ArgumentCaptor<?> @NotNull [] captors = testSingleMethod(executor, method, staticObjects, target, invokedMethod, invokedMethodParamTypes);
captorsValidator.accept(captors);
// Clean up
try {
MockingProgress mockingProgress = mockingProgress();
mockingProgress.validateState();
mockingProgress.reset();
mockingProgress.resetOngoingStubbing();
reset(target);
} catch (NotAMockException ignored) {}
}
}
/**
* Allows testing the given <i>targetMethod</i>.
* It first invokes <i>targetMethod</i> from <i>executor</i>
* with the given static objects as parameters.
* Then, it verifies using <b>Mock</b> that the <i>target</i> object
* invoked <i>invokedMethod</i> during the execution of the prior method.
*
* @param executor the executor
* @param targetMethod the target method
* @param staticObjects the objects that will be used for the creation of the parameters of <i>targetMethod</i>. If the required class is present among these objects, then the one provided will be used. Otherwise, {@link #mockParameter(Class)} will be called.
* @param target the target
* @param invokedMethod the invoked method
* @param invokedMethodParamTypes the type of the parameters when invoking <i>invokedMethod</i>.
* These will also be the types of the returned captors
* @return the argument captors of the invoked parameters
*/
public static ArgumentCaptor<?> @NotNull [] testSingleMethod(final @NotNull Object executor, final @NotNull Method targetMethod,
final Object @NotNull [] staticObjects,
final @NotNull Object target, final String invokedMethod,
final Class<?> @NotNull ... invokedMethodParamTypes) {
// Prepare argument captors
final ArgumentCaptor<?>[] captors = initializeCaptors(invokedMethodParamTypes);
try {
// Execute target method
final Object[] parameters = initializeParameters(targetMethod.getParameterTypes(), staticObjects);
ReflectionUtils.setAccessibleOrThrow(targetMethod).invoke(executor, parameters);
// Verify execution with mock
Method method = target.getClass().getDeclaredMethod(invokedMethod, invokedMethodParamTypes);
ReflectionUtils.setAccessibleOrThrow(method).invoke(verify(target),
Arrays.stream(captors).map(ArgumentCaptor::capture).toArray(Object[]::new));
return captors;
} catch (Exception e) {
System.err.printf("An exception occurred while testing method '%s'%n", methodToString(targetMethod));
System.err.printf("target: '%s'%n", target.getClass().getCanonicalName());
System.err.printf("method: '%s'%n", ReflectionUtils.getMethod(target.getClass(), null, invokedMethod,
invokedMethodParamTypes));
System.err.printf("Invoked method parameter types: '%s'%n", Arrays.toString(invokedMethodParamTypes));
System.err.printf("Captors: '%s'%n", Arrays.toString(captors));
ExceptionUtils.throwException(e);
return null;
}
}
private static Object[] initializeParameters(final Class<?> @NotNull [] classes, final Object @NotNull ... staticObjects) {
return Arrays.stream(classes).map(c -> initializeSingle(c, staticObjects)).toArray(Object[]::new);
}
private static Object initializeSingle(final Class<?> clazz, final Object @NotNull ... staticObjects) {
for (Object o : staticObjects) if (clazz.isAssignableFrom(o.getClass())) return o;
return TestUtils.mockParameter(clazz);
}
private static ArgumentCaptor<?>[] initializeCaptors(final Class<?> @NotNull [] classes) {
return Arrays.stream(classes).map(ArgumentCaptor::forClass).toArray(ArgumentCaptor[]::new);
}
/**
* Many objects have setter, adder or remover methods which return the object itself,
* to allow method chaining.
* This function allows checking each one to verify that the return type is consistent with the original object.
*
* @param <T> the type parameter
* @param object the object
* @param clazz the class of interest. If there are more implementations of the object, here there should be the most abstract one.
* @param filter if there are some methods that return a copy or a clone of the object, they should be filtered here.
*/
public static <T> void testReturnType(final @NotNull T object, final @NotNull Class<? super T> clazz,
final @Nullable Predicate<Method> filter) {
testReturnType(object, clazz, object.getClass(), filter);
}
/**
* Many objects have setter, adder or remover methods which return the object itself,
* to allow method chaining.
* This function allows checking each one to verify that the return type is consistent with the original object.
*
* @param <T> the type parameter
* @param object the object
* @param clazz the class of interest. If there are more implementations of the object, here there should be the most abstract one.
* @param expectedReturnType the expected return type of the methods.
* For example, if the object is a hidden implementation,
* the corresponding abstract class (or interface) should be passed.
* @param filter if there are some methods that return a copy or a clone of the object, they should be filtered here.
*/
public static <T> void testReturnType(final @NotNull T object, final @NotNull Class<? super T> clazz,
@NotNull Class<?> expectedReturnType,
final @Nullable Predicate<Method> filter) {
if (expectedReturnType.getSimpleName().endsWith("Impl"))
try {
String name = expectedReturnType.getCanonicalName();
expectedReturnType = ReflectionUtils.getClass(name.substring(0, name.length() - "Impl".length()));
} catch (IllegalArgumentException ignored) {}
for (Method method : clazz.getDeclaredMethods()) {
final Class<?>[] parameters = method.getParameterTypes();
final String methodString = methodToString(method);
try {
final Class<?> returnType = method.getReturnType();
if (Modifier.isStatic(method.getModifiers())) continue;
if (!clazz.isAssignableFrom(returnType)) continue;
if (filter != null && filter.test(method)) continue;
final Class<?> objectClass = object.getClass();
final String objectClassName = objectClass.getSimpleName();
String errorMessage = String.format("Method '%s' of class '%s' did not return itself",
methodString, objectClassName);
Object[] mockParameters = Arrays.stream(parameters).map(TestUtils::mockParameter).toArray(Object[]::new);
Object o = ReflectionUtils.setAccessibleOrThrow(method).invoke(object, mockParameters);
if (method.getName().equals("copy"))
assertInstanceOf(objectClass, o, String.format("Returned object from %s call should have been %s but was %s",
methodString, objectClass, o.getClass()));
else {
try {
ReflectionUtils.getMethod(objectClass, expectedReturnType, method.getName(), method.getParameterTypes());
} catch (IllegalArgumentException e) {
fail(String.format("Method '%s' of class '%s' did not have return type of '%s'",
methodString, objectClassName, objectClassName));
}
assertEquals(object.hashCode(), o.hashCode(), errorMessage);
}
} catch (Exception e) {
System.err.printf("An exception occurred while testing method '%s'%n", methodString);
ExceptionUtils.throwException(e);
}
}
}
/**
* Mocks the given class to an object.
*
* @param clazz the clazz
* @return the object
*/
public static Object mockParameter(Class<?> clazz) {
clazz = ReflectionUtils.getWrapperClass(clazz);
if (Number.class.isAssignableFrom(clazz)) return 1;
if (String.class.isAssignableFrom(clazz)) return "STONE";
if (Boolean.class.isAssignableFrom(clazz)) return false;
if (clazz.isEnum()) {
Enum<?>[] enums = new Refl<>(clazz).invokeMethod("values");
return enums[0];
}
if (clazz.isArray()) return Array.newInstance(clazz.getComponentType(), 0);
if (Collection.class.isAssignableFrom(clazz)) return new ArrayList<>();
Object object = mock(clazz);
if (clazz.getPackage().getName().endsWith("guis"))
when(new Refl<>(object).invokeMethod("size")).thenReturn(9);
return object;
}
private static String methodToString(final @NotNull Method method) {
return String.format("%s(%s)", method.getName(), Arrays.stream(method.getParameterTypes())
.map(Class::getSimpleName).collect(Collectors.joining(", ")));
}
}