aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java')
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java266
1 files changed, 266 insertions, 0 deletions
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
new file mode 100644
index 0000000..a859a4f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Annotation to define a test annotation used to have parameterized methods, in either a
+ * parameterized or non parameterized test.
+ *
+ * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example)
+ * for the type of the parameter, defining a member variable annotated with this annotation, and
+ * specifying the parameter with the same annotation for each test, or for the whole class, for
+ * example:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @ColorParameter({BLUE, WHITE, RED}) private Color color;
+ *
+ * @Test
+ * public void test() {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>An alternative is to use a method parameter for injection:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * @ColorParameter({BLUE, WHITE, RED})
+ * public void test(Color color) {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Yet another alternative is to use a method parameter for injection, but with the annotation
+ * specified on the parameter itself, which helps when multiple arguments share the
+ * same @TestParameterAnnotation annotation.
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * public void test(@ColorParameter({BLUE, WHITE}) Color color1,
+ * @ColorParameter({WHITE, RED}) Color color2) {
+ * assertThat(paint(color1. color2)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown
+ * below:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * public @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) {
+ * ...
+ * }
+ *
+ * @Test
+ * public void test() {...}
+ * }
+ * }</pre>
+ *
+ * <p>Each field that needs to be injected from a parameter requires its dedicated distinct
+ * annotation.
+ *
+ * <p>If the same annotation is defined both on the class and method, the method parameter values
+ * take precedence.
+ *
+ * <p>If the same annotation is defined both on the class and constructor, the constructor parameter
+ * values take precedence.
+ *
+ * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a
+ * method or method parameter.
+ *
+ * <p>Since the parameter values must be specified in an annotation return value, they are
+ * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If
+ * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code
+ * Parameters} has to be used instead.
+ */
+@Retention(RUNTIME)
+@Target({ANNOTATION_TYPE})
+@interface TestParameterAnnotation {
+ /**
+ * Pattern of the {@link MessageFormat} format to derive the test's name from the parameters.
+ *
+ * @see {@code Parameters#name()}
+ */
+ String name() default "{0}";
+
+ /** Specifies a validator for the parameter to determine whether test should be skipped. */
+ Class<? extends TestParameterValidator> validator() default DefaultValidator.class;
+
+ /**
+ * Specifies a processor for the parameter to invoke arbitrary code before and after the test
+ * statement's execution.
+ */
+ Class<? extends TestParameterProcessor> processor() default DefaultProcessor.class;
+
+ /** Specifies a value provider for the parameter to provide the values to test. */
+ Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class;
+
+ /** Default {@link TestParameterValidator} implementation which skips no test. */
+ class DefaultValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ return false;
+ }
+ }
+
+ /** Default {@link TestParameterProcessor} implementation which does nothing. */
+ class DefaultProcessor implements TestParameterProcessor {
+ @Override
+ public void before(Object testParameterValue) {}
+
+ @Override
+ public void after(Object testParameterValue) {}
+ }
+
+ /**
+ * Default {@link TestParameterValueProvider} implementation that gets its values from the
+ * annotation's `value` method.
+ */
+ class DefaultValueProvider implements TestParameterValueProvider {
+
+ @Override
+ public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
+ Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType());
+ checkState(
+ parameters.getClass().isArray(),
+ "The return value of the value method should be an array");
+
+ int parameterCount = Array.getLength(parameters);
+ ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < parameterCount; i++) {
+ Object value = Array.get(parameters, i);
+ if (parameterClass.isPresent()) {
+ verify(
+ Primitives.wrap(parameterClass.get()).isInstance(value),
+ "Found %s annotation next to a parameter of type %s which doesn't match"
+ + " (annotation = %s)",
+ annotation.annotationType().getSimpleName(),
+ parameterClass.get().getSimpleName(),
+ annotation);
+ }
+ resultBuilder.add(value);
+ }
+ return resultBuilder.build();
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ try {
+ Method valueMethod = annotationType.getMethod("value");
+ return valueMethod.getReturnType().getComponentType();
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ }
+
+ /**
+ * Returns the parameters of the test parameter, by calling the {@code value} method on the
+ * annotation.
+ */
+ private static Object getParametersAnnotationValues(
+ Annotation annotation, Class<? extends Annotation> annotationType) {
+ Method valueMethod;
+ try {
+ valueMethod = annotationType.getMethod("value");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ Object parameters;
+ try {
+ parameters = valueMethod.invoke(annotation);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof IllegalAccessError) {
+ // There seems to be a bug or at least something weird with the JVM that causes
+ // IllegalAccessError to be thrown because the return value is not visible when it is a
+ // non-public nested type. See
+ // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more
+ // info.
+ throw new RuntimeException(
+ String.format(
+ "Could not access %s.value(). This is probably because %s is not visible to the"
+ + " annotation proxy. To fix this, make %s public.",
+ annotationType.getSimpleName(),
+ valueMethod.getReturnType().getSimpleName(),
+ valueMethod.getReturnType().getSimpleName()));
+ // Note: Not chaining the exception to reduce the clutter for the reader
+ } else {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ return parameters;
+ }
+ }
+}