blob: a859a4f84d54fe800e5254b196af563a63c583da [file] [log] [blame]
/*
* 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;
}
}
}