aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java')
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java224
1 files changed, 224 insertions, 0 deletions
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
new file mode 100644
index 0000000..6725d16
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
@@ -0,0 +1,224 @@
+/*
+ * 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 java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import com.google.protobuf.MessageLite;
+import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Test parameter annotation that defines the values that a single parameter can have.
+ *
+ * <p>For enums and booleans, the values can be automatically derived as all possible values:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void test1(@TestParameter MyEnum myEnum, @TestParameter boolean myBoolean) {
+ * // ... will run for [(A,false), (A,true), (B,false), (B,true), (C,false), (C,true)]
+ * }
+ *
+ * enum MyEnum { A, B, C }
+ * </pre>
+ *
+ * <p>The values can be explicitly defined as a parsed string:
+ *
+ * <pre>
+ * public void test1(
+ * {@literal @}TestParameter({"{name: Hermione, age: 18}", "{name: Dumbledore, age: 115}"})
+ * UpdateCharacterRequest request,
+ * {@literal @}TestParameter({"1", "4"}) int bookNumber) {
+ * // ... will run for [(Hermione,1), (Hermione,4), (Dumbledore,1), (Dumbledore,4)]
+ * }
+ * </pre>
+ *
+ * <p>For more flexibility, see {{@link #valuesProvider()}}. If you don't want to test all possible
+ * combinations but instead want to specify sets of parameters explicitly, use @{@link
+ * TestParameters}.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, PARAMETER})
+@TestParameterAnnotation(valueProvider = InternalImplementationOfThisParameter.class)
+public @interface TestParameter {
+
+ /**
+ * Array of stringified values for the annotated type.
+ *
+ * <p>Types that are supported:
+ *
+ * <ul>
+ * <li>String: No parsing happens
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes
+ * (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ *
+ * <p>For examples, see {@link TestParameter}.
+ */
+ String[] value() default {};
+
+ /**
+ * Sets a provider that will return a list of parameter values.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void matchesAllOf_throwsOnNull(
+ * {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
+ * CharMatcher charMatcher) {
+ * assertThrows(NullPointerException.class, () -&gt; charMatcher.matchesAllOf(null));
+ * }
+ *
+ * private static final class CharMatcherProvider implements TestParameterValuesProvider {
+ * {@literal @}Override
+ * public {@literal List<CharMatcher>} provideValues() {
+ * return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParameterValuesProvider> valuesProvider() default
+ DefaultTestParameterValuesProvider.class;
+
+ /** Interface for custom providers of test parameter values. */
+ interface TestParameterValuesProvider {
+ List<?> provideValues();
+ }
+
+ /** Default {@link TestParameterValuesProvider} implementation that does nothing. */
+ class DefaultTestParameterValuesProvider implements TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+
+ /** Implementation of this parameter annotation. */
+ final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
+ @Override
+ public List<Object> provideValues(
+ Annotation uncastAnnotation, Optional<Class<?>> maybeParameterClass) {
+ TestParameter annotation = (TestParameter) uncastAnnotation;
+ Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass);
+
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParameterValuesProvider.class);
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider on annotation %s",
+ annotation);
+
+ if (valueIsSet) {
+ return stream(annotation.value())
+ .map(v -> parseStringValue(v, parameterClass))
+ .collect(toList());
+ } else if (valuesProviderIsSet) {
+ return getValuesFromProvider(annotation.valuesProvider());
+ } else {
+ if (Enum.class.isAssignableFrom(parameterClass)) {
+ return ImmutableList.copyOf(parameterClass.asSubclass(Enum.class).getEnumConstants());
+ } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) {
+ return ImmutableList.of(false, true);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "A @TestParameter without values can only be placed at an enum or a boolean, but"
+ + " was placed by a %s",
+ parameterClass));
+ }
+ }
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ return parameterClass.orElseThrow(
+ () ->
+ new AssertionError(
+ String.format(
+ "An empty parameter class should not be possible since"
+ + " @TestParameter can only target FIELD or PARAMETER, both"
+ + " of which are supported for annotation %s.",
+ annotationType)));
+ }
+
+ private static Object parseStringValue(String value, Class<?> parameterClass) {
+ if (parameterClass.equals(String.class)) {
+ return value.equals("null") ? null : value;
+ } else if (Enum.class.isAssignableFrom(parameterClass)) {
+ return value.equals("null") ? null : ParameterValueParsing.parseEnum(value, parameterClass);
+ } else if (MessageLite.class.isAssignableFrom(parameterClass)) {
+ if (ParameterValueParsing.isValidYamlString(value)) {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ } else {
+ return ParameterValueParsing.parseTextprotoMessage(value, parameterClass);
+ }
+ } else {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ }
+ }
+
+ private static List<Object> getValuesFromProvider(
+ Class<? extends TestParameterValuesProvider> valuesProvider) {
+ try {
+ Constructor<? extends TestParameterValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return new ArrayList<>(constructor.newInstance().provideValues());
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+}