diff options
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.java | 224 |
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, () -> 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); + } + } + } +} |