Skip to content

Commit

Permalink
Implement context aware TestParameterValuesProvider.
Browse files Browse the repository at this point in the history
Fixes #44
  • Loading branch information
nymanjens committed Jan 16, 2024
1 parent f82c1ff commit a73e3f4
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter;
import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -147,7 +148,10 @@ public List<Object> provideValues() {
final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
@Override
public List<Object> provideValues(
Annotation uncastAnnotation, Optional<Class<?>> maybeParameterClass) {
Annotation uncastAnnotation,
ImmutableList<Annotation> otherAnnotations,
Optional<Class<?>> maybeParameterClass,
Class<?> testClass) {
TestParameter annotation = (TestParameter) uncastAnnotation;
Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass);

Expand All @@ -165,7 +169,8 @@ public List<Object> provideValues(
.transform(v -> parseStringValue(v, parameterClass))
.toArray(Object.class));
} else if (valuesProviderIsSet) {
return getValuesFromProvider(annotation.valuesProvider());
return getValuesFromProvider(
annotation.valuesProvider(), Context.create(otherAnnotations, testClass));
} else {
if (Enum.class.isAssignableFrom(parameterClass)) {
return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants());
Expand Down Expand Up @@ -206,12 +211,21 @@ private static Object parseStringValue(String value, Class<?> parameterClass) {
}

private static List<Object> getValuesFromProvider(
Class<? extends TestParameterValuesProvider> valuesProvider) {
Class<? extends TestParameterValuesProvider> valuesProvider, Context context) {
try {
Constructor<? extends TestParameterValuesProvider> constructor =
valuesProvider.getDeclaredConstructor();
constructor.setAccessible(true);
return new ArrayList<>(constructor.newInstance().provideValues());
TestParameterValuesProvider instance = constructor.newInstance();
if (instance
instanceof com.google.testing.junit.testparameterinjector.TestParameterValuesProvider) {
return new ArrayList<>(
((com.google.testing.junit.testparameterinjector.TestParameterValuesProvider)
instance)
.provideValues(context));
} else {
return new ArrayList<>(instance.provideValues());
}
} catch (NoSuchMethodException e) {
if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
throw new IllegalStateException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2024 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 com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Annotation;
import java.util.List;
import javax.annotation.Nullable;

/**
* Abstract class for custom providers of @TestParameter values.
*
* <p>This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon
* be deprecated. The difference with the former interface is that this class provides a {@code
* Context} instance when invoking {@link #provideValues}.
*/
public abstract class TestParameterValuesProvider
implements TestParameter.TestParameterValuesProvider {

abstract List<?> provideValues(Context context);

@Override
public final List<?> provideValues() {
throw new UnsupportedOperationException(
"The TestParameterInjector framework should never call this method, and instead call"
+ " #provideValues(Context)");
}

/**
* Wraps the given value in an object that allows you to give the parameter value a different
* name. The TestParameterInjector framework will recognize the returned {@link
* TestParameterValue} instances and unwrap them at injection time.
*
* <p>Usage: {@code value(file.content).withName(file.name)}.
*/
@Override
public final TestParameterValue value(@Nullable Object wrappedValue) {
// Overriding this method as final because it is not supposed to be overwritten
return TestParameter.TestParameterValuesProvider.super.value(wrappedValue);
}

/**
* An immutable value class that contains extra information about the context of the parameter for
* which values are being provided.
*/
@AutoValue
public abstract static class Context {
/**
* A list of all other annotations on the field or parameter that was annotated
* with @TestParameter.
*
* <p>For example, if the test code is as follows:
*
* <pre>{@code
* @Test
* public void myTest_success(
* @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
* ...
* }
* }</pre>
*
* then this list will contain a single element: @CustomAnnotation(123).
*/
public abstract ImmutableList<Annotation> otherAnnotations();

/**
* The class that contains the test that is currently being run.
*
* <p>Having this can be useful when sharing providers between tests that have the same base
* class. In those cases, an abstract method can be called as follows:
*
* <pre>
* ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
* </pre>
*/
public abstract Class<?> testClass();

static Context create(ImmutableList<Annotation> otherAnnotations, Class<?> testClass) {
return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass);
}

@Override
public final String toString() {
return String.format(
"Context(otherAnnotations=[%s],testClass=%s)",
FluentIterable.from(otherAnnotations()).join(Joiner.on(',')),
testClass().getSimpleName());
}

Context() {} // Prevent implementations outside of this package
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.base.CharMatcher;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase;
import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider;
import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.inject.Qualifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
Expand Down Expand Up @@ -196,29 +200,101 @@ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
.build();
}

private static final class TestNumberProvider implements TestParameterValuesProvider {
private static final class TestNumberProvider
implements TestParameter.TestParameterValuesProvider {
@Override
public List<?> provideValues() {
return newArrayList(value(1).withName("one"), 2);
}
}

private static final class TestStringProvider implements TestParameterValuesProvider {
private static final class TestStringProvider
implements TestParameter.TestParameterValuesProvider {
@Override
public List<?> provideValues() {
return newArrayList(
"A", "B", null, value(null).withName("nothing"), value("harry").withName("wizard"));
}
}

private static final class CharMatcherProvider implements TestParameterValuesProvider {
private static final class CharMatcherProvider
implements TestParameter.TestParameterValuesProvider {
@Override
public List<CharMatcher> provideValues() {
return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
}
}
}

@RunAsTest
public static class WithContextAwareValuesProvider extends SuccessfulTestCaseBase {

@CustomFieldAnnotation
@TestParameter(valuesProvider = InjectContextProvider.class)
private Context contextFromField;

private final Context contextFromConstructor;

public WithContextAwareValuesProvider(
@TestParameter(valuesProvider = InjectContextProvider.class) Context context) {
this.contextFromConstructor = context;
}

@Test
public void contextTest(
@CustomParameterAnnotation1
@CustomParameterAnnotation2
@TestParameter(valuesProvider = InjectContextProvider.class)
Context contextFromParameter) {
assertThat(contextFromField.testClass()).isEqualTo(WithContextAwareValuesProvider.class);
assertThat(contextFromConstructor.testClass())
.isEqualTo(WithContextAwareValuesProvider.class);
assertThat(contextFromParameter.testClass()).isEqualTo(WithContextAwareValuesProvider.class);

assertThat(
FluentIterable.from(contextFromField.otherAnnotations())
.transform(Annotation::annotationType)
.toList())
.containsExactly(CustomFieldAnnotation.class);
assertThat(contextFromConstructor.otherAnnotations()).isEmpty();
assertThat(
FluentIterable.from(contextFromParameter.otherAnnotations())
.transform(Annotation::annotationType)
.toList())
.containsExactly(CustomParameterAnnotation1.class, CustomParameterAnnotation2.class);

storeTestParametersForThisTest(contextFromParameter);
}

@Override
ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
return ImmutableMap.<String, String>builder()
.put(
"contextTest[1.Context(otherAnnotations=[@com.google.testing.junit.tes...,1.Context(otherAnnotations=[],testClass=WithContextAwareV...,1.Context(otherAnnotations=[@com.google.testing.junit.tes...]",
"Context(otherAnnotations=[@com.google.testing.junit.testparameterinjector.TestParameterTest.WithContextAwareValuesProvider.CustomParameterAnnotation1(),@com.google.testing.junit.testparameterinjector.TestParameterTest.WithContextAwareValuesProvider.CustomParameterAnnotation2()],testClass=WithContextAwareValuesProvider)")
.build();
}

private static final class InjectContextProvider extends TestParameterValuesProvider {
@Override
public List<?> provideValues(Context context) {
return newArrayList(context);
}
}

@Qualifier
@Retention(RUNTIME)
@interface CustomFieldAnnotation {}

@Qualifier
@Retention(RUNTIME)
@interface CustomParameterAnnotation1 {}

@Qualifier
@Retention(RUNTIME)
@interface CustomParameterAnnotation2 {}
}

@Parameters(name = "{0}")
public static Collection<Object[]> parameters() {
return Arrays.stream(TestParameterTest.class.getClasses())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter;
import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider.Context;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -147,7 +148,10 @@ public List<Object> provideValues() {
final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
@Override
public List<Object> provideValues(
Annotation uncastAnnotation, Optional<Class<?>> maybeParameterClass) {
Annotation uncastAnnotation,
ImmutableList<Annotation> otherAnnotations,
Optional<Class<?>> maybeParameterClass,
Class<?> testClass) {
TestParameter annotation = (TestParameter) uncastAnnotation;
Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass);

Expand All @@ -165,7 +169,8 @@ public List<Object> provideValues(
.transform(v -> parseStringValue(v, parameterClass))
.toArray(Object.class));
} else if (valuesProviderIsSet) {
return getValuesFromProvider(annotation.valuesProvider());
return getValuesFromProvider(
annotation.valuesProvider(), Context.create(otherAnnotations, testClass));
} else {
if (Enum.class.isAssignableFrom(parameterClass)) {
return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants());
Expand Down Expand Up @@ -206,12 +211,21 @@ private static Object parseStringValue(String value, Class<?> parameterClass) {
}

private static List<Object> getValuesFromProvider(
Class<? extends TestParameterValuesProvider> valuesProvider) {
Class<? extends TestParameterValuesProvider> valuesProvider, Context context) {
try {
Constructor<? extends TestParameterValuesProvider> constructor =
valuesProvider.getDeclaredConstructor();
constructor.setAccessible(true);
return new ArrayList<>(constructor.newInstance().provideValues());
TestParameterValuesProvider instance = constructor.newInstance();
if (instance
instanceof com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider) {
return new ArrayList<>(
((com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider)
instance)
.provideValues(context));
} else {
return new ArrayList<>(instance.provideValues());
}
} catch (NoSuchMethodException e) {
if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
throw new IllegalStateException(
Expand Down
Loading

0 comments on commit a73e3f4

Please sign in to comment.