From 53043ddc0d7706c1538fb7f877848ecbcd52ed00 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 21 May 2016 11:54:54 +0200 Subject: [PATCH] Create tool task that checks Mock fields of test classes --- .../initialization/ConstructorInjection.java | 2 +- .../authme/initialization/FieldInjection.java | 2 +- .../authme/initialization/Injection.java | 2 +- .../authme/command/CommandServiceTest.java | 3 - src/test/java/tools/ToolsRunner.java | 6 +- .../tools/checktestmocks/CheckTestMocks.java | 171 ++++++++++++++++++ src/test/java/tools/utils/ToolsConstants.java | 3 + 7 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 src/test/java/tools/checktestmocks/CheckTestMocks.java diff --git a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java index e80ea128c..74ea28b2e 100644 --- a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java @@ -11,7 +11,7 @@ import java.lang.reflect.InvocationTargetException; /** * Functionality for constructor injection. */ -class ConstructorInjection implements Injection { +public class ConstructorInjection implements Injection { private final Constructor constructor; diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index b8112b876..e9717b334 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -16,7 +16,7 @@ import java.util.List; /** * Functionality for field injection. */ -class FieldInjection implements Injection { +public class FieldInjection implements Injection { private final Field[] fields; private final Constructor defaultConstructor; diff --git a/src/main/java/fr/xephi/authme/initialization/Injection.java b/src/main/java/fr/xephi/authme/initialization/Injection.java index 6e85b4ce5..65acf7961 100644 --- a/src/main/java/fr/xephi/authme/initialization/Injection.java +++ b/src/main/java/fr/xephi/authme/initialization/Injection.java @@ -5,7 +5,7 @@ package fr.xephi.authme.initialization; * * @param the type of the concerned object */ -interface Injection { +public interface Injection { /** * Returns the dependencies that must be provided to instantiate the given item. diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index ba7e4d281..c07d44d5d 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -6,7 +6,6 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -45,8 +44,6 @@ public class CommandServiceTest { private NewSetting settings; @Mock private ValidationService validationService; - @Mock - private BukkitService bukkitService; @Test public void shouldSendMessage() { diff --git a/src/test/java/tools/ToolsRunner.java b/src/test/java/tools/ToolsRunner.java index 9986e9819..f8e74aeaa 100644 --- a/src/test/java/tools/ToolsRunner.java +++ b/src/test/java/tools/ToolsRunner.java @@ -81,7 +81,7 @@ public final class ToolsRunner { private static void collectTasksInDirectory(File dir, Map taskCollection) { File[] files = dir.listFiles(); if (files == null) { - throw new RuntimeException("Cannot read folder '" + dir + "'"); + throw new IllegalStateException("Cannot read folder '" + dir + "'"); } for (File file : files) { if (file.isDirectory()) { @@ -112,7 +112,7 @@ public final class ToolsRunner { return constructor.newInstance(); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { - throw new RuntimeException("Cannot instantiate task '" + taskClass + "'"); + throw new IllegalStateException("Cannot instantiate task '" + taskClass + "'"); } } @@ -137,7 +137,7 @@ public final class ToolsRunner { ? (Class) clazz : null; } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } } diff --git a/src/test/java/tools/checktestmocks/CheckTestMocks.java b/src/test/java/tools/checktestmocks/CheckTestMocks.java new file mode 100644 index 000000000..2508cdd6f --- /dev/null +++ b/src/test/java/tools/checktestmocks/CheckTestMocks.java @@ -0,0 +1,171 @@ +package tools.checktestmocks; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.google.common.collect.Sets; +import fr.xephi.authme.initialization.ConstructorInjection; +import fr.xephi.authme.initialization.FieldInjection; +import fr.xephi.authme.initialization.Injection; +import fr.xephi.authme.util.StringUtils; +import org.mockito.Mock; +import tools.utils.AutoToolTask; +import tools.utils.ToolsConstants; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +/** + * Task checking if all tests' {@code @Mock} fields have a corresponding + * {@code @Inject} field in the class they are testing. + */ +public class CheckTestMocks implements AutoToolTask { + + private List errors = new ArrayList<>(); + + @Override + public String getTaskName() { + return "checkTestMocks"; + } + + @Override + public void execute(Scanner scanner) { + executeDefault(); + } + + @Override + public void executeDefault() { + readAndCheckFiles(new File(ToolsConstants.TEST_SOURCE_ROOT)); + System.out.println(StringUtils.join("\n", errors)); + } + + /** + * Recursively reads directories and checks the contained classes. + * + * @param dir the directory to read + */ + private void readAndCheckFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalStateException("Cannot read folder '" + dir + "'"); + } + for (File file : files) { + if (file.isDirectory()) { + readAndCheckFiles(file); + } else if (file.isFile()) { + Class clazz = loadTestClass(file); + if (clazz != null) { + checkClass(clazz); + } + // else System.out.format("No @Mock fields found in class of file '%s'%n", file.getName()) + } + } + } + + /** + * Checks the given test class' @Mock fields against the corresponding production class' @Inject fields. + * + * @param testClass the test class to verify + */ + private void checkClass(Class testClass) { + Class realClass = returnRealClass(testClass); + if (realClass != null) { + Set> mockFields = getMocks(testClass); + Set> injectFields = getRealClassDependencies(realClass); + if (!injectFields.containsAll(mockFields)) { + addErrorEntry(testClass, "Error - Found the following mocks absent as @Inject: " + + formatClassList(Sets.difference(mockFields, injectFields))); + } else if (!mockFields.containsAll(injectFields)) { + addErrorEntry(testClass, "Info - Found @Inject fields which are not present as @Mock: " + + formatClassList(Sets.difference(injectFields, mockFields))); + } + } + } + + private void addErrorEntry(Class clazz, String message) { + errors.add(clazz.getSimpleName() + ": " + message); + } + + private static Class loadTestClass(File file) { + String fileName = file.getPath(); + String className = fileName + // Strip source folders and .java ending + .substring("src/test/java/".length(), fileName.length() - 5) + .replace(File.separator, "."); + try { + Class clazz = CheckTestMocks.class.getClassLoader().loadClass(className); + return isTestClassWithMocks(clazz) ? clazz : null; + } catch (ClassNotFoundException e) { + throw new UnsupportedOperationException(e); + } + } + + private static Set> getMocks(Class clazz) { + Set> result = new HashSet<>(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Mock.class)) { + result.add(field.getType()); + } + } + return result; + } + + /** + * Returns the production class ("real class") corresponding to the given test class. + * Returns null if the production class could not be mapped or loaded. + * + * @param testClass the test class to find the corresponding production class for + * @return production class, or null if not found + */ + private static Class returnRealClass(Class testClass) { + String testClassName = testClass.getName(); + String realClassName = testClassName.replaceAll("(Integration|Consistency)?Test$", ""); + if (realClassName.equals(testClassName)) { + System.out.format("%s doesn't match typical test class naming pattern.%n", testClassName); + return null; + } + try { + return CheckTestMocks.class.getClassLoader().loadClass(realClassName); + } catch (ClassNotFoundException e) { + System.out.format("Real class '%s' not found for test class '%s'%n", realClassName, testClassName); + return null; + } + } + + private static Set> getRealClassDependencies(Class realClass) { + Injection injection = ConstructorInjection.provide(realClass).get(); + if (injection != null) { + return Sets.newHashSet(injection.getDependencies()); + } + injection = FieldInjection.provide(realClass).get(); + return injection == null + ? Collections.>emptySet() + : Sets.newHashSet(injection.getDependencies()); + } + + private static boolean isTestClassWithMocks(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Mock.class)) { + return true; + } + } + return false; + } + + private static String formatClassList(Collection> coll) { + Collection classNames = Collections2.transform(coll, new Function, String>() { + @Override + public String apply(Class input) { + return input.getSimpleName(); + } + }); + return StringUtils.join(", ", classNames); + } + +} diff --git a/src/test/java/tools/utils/ToolsConstants.java b/src/test/java/tools/utils/ToolsConstants.java index 84ad7fadf..c94a22b1f 100644 --- a/src/test/java/tools/utils/ToolsConstants.java +++ b/src/test/java/tools/utils/ToolsConstants.java @@ -12,6 +12,9 @@ public final class ToolsConstants { public static final String MAIN_RESOURCES_ROOT = "src/main/resources/"; + // Add specific `fr.xephi.authme` package as not to include the tool tasks in the `tools` package + public static final String TEST_SOURCE_ROOT = "src/test/java/fr/xephi/authme"; + public static final String TOOLS_SOURCE_ROOT = "src/test/java/tools/"; public static final String DOCS_FOLDER = "docs/";