TestParameterInjector is a JUnit4 and JUnit5 test runner that runs its test methods for
different combinations of field/parameter values.
Parameterized tests are a great way to avoid code duplication between tests and promote high test coverage for data-driven tests.
There are a lot of alternative parameterized test frameworks, such as
junit.runners.Parameterized
and JUnitParams. We believe
TestParameterInjector is an improvement of those because it is more powerful
and simpler to use.
This blogpost
goes into a bit more detail about how TestParameterInjector compares to other
frameworks used at Google.
JUnit4 (Java)
To start using TestParameterInjector right away, copy the following snippet:
importcom.google.testing.junit.testparameterinjector.TestParameterInjector;
importcom.google.testing.junit.testparameterinjector.TestParameter;
importcom.google.testing.junit.testparameterinjector.TestParameters;
@RunWith(TestParameterInjector.class)
publicclassMyTest {
@TestParameterbooleanisDryRun;
enumFetchResponseCode { FOUND, NOT_FOUND, ERROR }
@Testpublicvoidtest1(@TestParameterFetchResponseCoderesponseCode) {
// This test method is run 6 times for all combinations of isDryRun and responseCode
}
@Testpublicvoidtest2(
@TestParameterbooleanwithDeadline,
@TestParameter({"20", "100"}) intlimit) {
// This test method is run 2*2*2=8 times
}
@Test@TestParameters("{age: 17, expectIsAdult: false}")
@TestParameters("{age: 22, expectIsAdult: true}")
publicvoidtest3(intage, booleanexpectIsAdult) {
// This test method is run 4 times:// - isDryRun=false, age=17, expectIsAdult=false// - isDryRun=false, age=22, expectIsAdult=true// - isDryRun=true, age=17, expectIsAdult=false// - isDryRun=true, age=22, expectIsAdult=true
}
}And add the following dependency to your .pom file:
<dependency>
<groupId>com.google.testparameterinjector</groupId>
<artifactId>test-parameter-injector</artifactId>
<version>1.22</version>
<scope>test</scope>
</dependency>or see this Maven page for instructions for other build tools.
Additionally, Java needs to be compiled with the -parameters compiler option.
This can be done for example:
- In Maven with the
maven-compiler-plugin: Add<parameters>true</parameters>to<configuration> - With Gradle: Add
options.compilerArgs << "-parameters"
JUnit4 (Kotlin)
To start using TestParameterInjector right away, copy the following snippet:
importcom.google.testing.junit.testparameterinjector.TestParameterInjectorimportcom.google.testing.junit.testparameterinjector.TestParameterimportcom.google.testing.junit.testparameterinjector.KotlinTestParameters.testValues
@RunWith(TestParameterInjector::class)
classMyTest(
@TestParameter valisDryRun:Boolean
) {
enumclassFetchResponseCode { FOUND, NOT_FOUND, ERROR }
@Test
funtest1(@TestParameter responseCode:FetchResponseCode) {
// This test method is run 6 times for all combinations of isDryRun and responseCode
}
@Test
funtest2(
@TestParameter withDeadline:Boolean
@TestParameter limit:Int = testValues(20, 100)) {
// This test method is run 2*2*2=8 times
}
data classAgeCheckTestCase(valage:Int, valexpectIsAdult:Boolean)
@Test
funtest3(
@TestParameter testCase:AgeCheckTestCase = testValues(
AgeCheckTestCase(17, false),
AgeCheckTestCase(22, true),
)
) {
// This test method is run 4 times:// - isDryRun=false, testCase=AgeCheckTestCase(17, false)// - isDryRun=false, testCase=AgeCheckTestCase(22, true)// - isDryRun=true, testCase=AgeCheckTestCase(17, false)// - isDryRun=true, testCase=AgeCheckTestCase(22, true)
}
}And add the following dependency to your .pom file:
<dependency>
<groupId>com.google.testparameterinjector</groupId>
<artifactId>test-parameter-injector</artifactId>
<version>1.22</version>
<scope>test</scope>
</dependency>or see this Maven page for instructions for other build tools.
JUnit5 (Jupiter)
To start using TestParameterInjector right away, copy the following snippet:
importcom.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest;
importcom.google.testing.junit.testparameterinjector.junit5.TestParameter;
importcom.google.testing.junit.testparameterinjector.junit5.TestParameters;
classMyTest {
@TestParameterbooleanisDryRun;
enumFetchResponseCode { FOUND, NOT_FOUND, ERROR }
@TestParameterInjectorTestpublicvoidtest1(@TestParameterFetchResponseCoderesponseCode) {
// This test method is run 6 times for all combinations of isDryRun and responseCode
}
@TestParameterInjectorTestpublicvoidtest2(
@TestParameterbooleanwithDeadline,
@TestParameter({"20", "100"}) intlimit) {
// This test method is run 2*2*2=8 times
}
@TestParameterInjectorTest@TestParameters("{age: 17, expectIsAdult: false}")
@TestParameters("{age: 22, expectIsAdult: true}")
publicvoidtest3(intage, booleanexpectIsAdult) {
// This test method is run 4 times:// - isDryRun=false, age=17, expectIsAdult=false// - isDryRun=false, age=22, expectIsAdult=true// - isDryRun=true, age=17, expectIsAdult=false// - isDryRun=true, age=22, expectIsAdult=true
}
}And add the following dependency to your .pom file:
<dependency>
<groupId>com.google.testparameterinjector</groupId>
<artifactId>test-parameter-injector-junit5</artifactId>
<version>1.22</version>
<scope>test</scope>
</dependency>or see this Maven page for instructions for other build tools.
Additionally, Java needs to be compiled with the -parameters compiler option.
This can be done for example:
- In Maven with the
maven-compiler-plugin: Add<parameters>true</parameters>to<configuration> - With Gradle: Add
options.compilerArgs << "-parameters"
Note about JUnit4 vs JUnit5:
The code below assumes you're using JUnit4. For JUnit5 users, simply remove the
@RunWith annotation and replace @Test by @TestParameterInjectorTest.
The simplest way to use this library is to use @TestParameter. For example:
@RunWith(TestParameterInjector.class)
publicclassMyTest {
@Testpublicvoidtest(@TestParameterbooleanisOwner) {...}
}In this example, two tests will be automatically generated by the test framework:
- One with
isOwnerset totrue - One with
isOwnerset tofalse
When running the tests, the result will show the following test names:
MyTest#test[isOwner=true]
MyTest#test[isOwner=false]
@TestParameter can also annotate a field:
@RunWith(TestParameterInjector.class)
publicclassMyTest {
@TestParameterprivatebooleanisOwner;
@Testpublicvoidtest1() {...}
@Testpublicvoidtest2() {...}
}In this example, both test1 and test2 will be run twice (once for each
parameter value).
The test runner will set these fields before calling any methods, so it is safe
to use such @TestParameter-annotated fields for setting up other test values
and behavior in @Before methods.
The following examples show most of the supported types. See the
@TestParameter javadoc
for more details.
Java
// Enums@TestParameterAnimalEnuma; // Implies all possible values of AnimalEnum@TestParameter({"CAT", "DOG"}) AnimalEnuma; // Implies AnimalEnum.CAT and AnimalEnum.DOG.// Strings@TestParameter({"cat", "dog"}) StringanimalName;
// Java primitives@TestParameterbooleanb; // Implies {true, false}@TestParameter({"1", "2", "3"}) inti;
@TestParameter({"1", "1.5", "2"}) doubled;
// Bytes@TestParameter({"!!binary 'ZGF0YQ=='", "some_string"}) byte[] bytes;
// Durations (segments of number+unit as shown below)@TestParameter({"1d", "2h", "3min", "4s", "5ms", "6us", "7ns"}) java.time.Durationd;
@TestParameter({"1h30min", "-2h10min20s", "1.5h", ".5s", "0"}) java.time.Durationd;For non-primitive types (e.g. String, enums, bytes), "null" is always parsed as the null reference.
Kotlin
// Enums
@TestParameter a:AnimalEnum// Implies all possible values of AnimalEnum
@TestParameter a:AnimalEnum= testValues(AnimalEnum.CAT, AnimalEnum.DOG)
// Strings
@TestParameter animalName:String= testValues("cat", "dog")
// Primitives
@TestParameter b:Boolean// Implies {true, false}
@TestParameter i:Int= testValues(1, 2, 3)
@TestParameter d:Double= testValues(1.0, 1.5, 2.0)
// ... Any type is supported when using testValues()If there are multiple @TestParameter-annotated values applicable to one test
method, the test is run for all possible combinations of those values. Example:
@RunWith(TestParameterInjector.class)
publicclassMyTest {
@TestParameterprivatebooleana;
@Testpublicvoidtest1(@TestParameterbooleanb, @TestParameterbooleanc) {
// Run for these combinations:// (a=false, b=false, c=false)// (a=false, b=false, c=true )// (a=false, b=true, c=false)// (a=false, b=true, c=true )// (a=true, b=false, c=false)// (a=true, b=false, c=true )// (a=true, b=true, c=false)// (a=true, b=true, c=true )
}
}If you want to explicitly define which combinations are run, see the next sections.
Java
Use a test enum if you want to:
- Explicitly specify the combination of parameters
- or your parameters are too large to be encoded in a
Stringin a readable way
Example:
@RunWith(TestParameterInjector.class)
publicclassMyTest {
enumFruitVolumeTestCase {
APPLE(Fruit.newBuilder().setName("Apple").setShape(SPHERE).build(), /* expectedVolume= */3.1),
BANANA(Fruit.newBuilder().setName("Banana").setShape(CURVED).build(), /* expectedVolume= */2.1),
MELON(Fruit.newBuilder().setName("Melon").setShape(SPHERE).build(), /* expectedVolume= */6);
finalFruitfruit;
finaldoubleexpectedVolume;
FruitVolumeTestCase(Fruitfruit, doubleexpectedVolume) {
this.fruit = fruit;
this.expectedVolume = expectedVolume;
}
}
@TestpublicvoidcalculateVolume_success(@TestParameterFruitVolumeTestCasefruitVolumeTestCase) {
assertThat(calculateVolume(fruitVolumeTestCase.fruit))
.isEqualTo(fruitVolumeTestCase.expectedVolume);
}
}The enum constant name has the added benefit of making for sensible test names:
MyTest#calculateVolume_success[APPLE]
MyTest#calculateVolume_success[BANANA]
MyTest#calculateVolume_success[MELON]
Kotlin
To explicitly specify the combination of parameters, we recommend using a data class as follows:
@RunWith(TestParameterInjector::class)
classMyTest {
data classAgeCheckTestCase(valage:Int, valexpectIsAdult:Boolean)
@Test
funpersonIsAdult(
@TestParameter testCase:AgeCheckTestCase = testValues(
AgeCheckTestCase(17, false),
AgeCheckTestCase(22, true),
)
) { /*...*/ }
}Tip: Consider setting a custom name if the data class is non-trivial:
importcom.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues @Test funpersonIsAdult( @TestParameter testCase:AgeCheckTestCase = namedTestValues( "teenager" to AgeCheckTestCase(17, false), "young adult" to AgeCheckTestCase(22, true), ) ) { /*...*/ }
You can also explicitly enumerate the sets of test parameters via a list of YAML mappings:
@Test@TestParameters("{age: 17, expectIsAdult: false}")
@TestParameters("{age: 22, expectIsAdult: true}")
publicvoidpersonIsAdult(intage, booleanexpectIsAdult) { /*...*/ }which would generate the following tests:
MyTest#personIsAdult[{age: 17, expectIsAdult: false}]
MyTest#personIsAdult[{age: 22, expectIsAdult: true}]
The string format supports the same types as @TestParameter (e.g. enums). See
the @TestParameters javadoc
for more info.
@TestParameters works in the same way on the constructor, in which case all
tests will be run for the given parameter sets.
Tip: Consider setting a custom name if the YAML string is large:
@Test@TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}") @TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}") publicvoidpersonIsAdult(intage, booleanexpectIsAdult) { /*...*/ }This will generate the following test names:
MyTest#personIsAdult[teenager] MyTest#personIsAdult[young adult]
Note: The parameters string is parsed as YAML without knowing the target types. Therefore, strings that could be interpreted as numbers or booleans should be escaped.
For example:
@Test@TestParameters("{phoneNumber: +12155555555}") publicvoidparsePhoneNumber_success(StringphoneNumber) { /*...*/ }will result
phoneNumber = "12155555555", without the+prefix. You can fix this by surrounding the string value with quotes:@Test@TestParameters("{phoneNumber: '+12155555555'}") publicvoidparsePhoneNumber_success(StringphoneNumber) { /*...*/ }
Sometimes, you want to exclude a parameter or a combination of parameters. We recommend doing this via JUnit assumptions which is also supported by Truth:
importstaticcom.google.common.truth.TruthJUnit.assume;
@TestpublicvoidmyTest(@TestParameterFruitfruit) {
assume().that(fruit).isNotEqualTo(Fruit.BANANA);
// At this point, the test will only run for APPLE and CHERRY.// The BANANA case will silently be ignored.
}
enumFruit { APPLE, BANANA, CHERRY }Note that the above works regardless of what parameterization framework you choose.
Note about JUnit4 vs JUnit5:
The code below assumes you're using JUnit4. For JUnit5 users, simply remove the
@RunWith annotation and replace @Test by @TestParameterInjectorTest.
Java
Instead of providing a list of parsable strings, you can implement your own
TestParameterValuesProvider as follows:
importcom.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
@TestpublicvoidmatchesAllOf_throwsOnNull(
@TestParameter(valuesProvider = CharMatcherProvider.class) CharMatchercharMatcher) {
assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
}
privatestaticfinalclassCharMatcherProviderextendsTestParameterValuesProvider {
@OverrideprotectedImmutableList<CharMatcher> provideValues(Contextcontext) {
returnImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
}
}Notes:
The
provideValues()method can dynamically construct the returned list, e.g. by reading a file.There are no restrictions on the object types returned.
The
provideValues()method is called before@BeforeClass, so don't rely on any static state initialized in there.The returned objects'
toString()will be used for the test names. If you want to customize the value names, you can do that as follows:privatestaticfinalclassFruitProviderextendsTestParameterValuesProvider { @OverrideprotectedImmutableList<?> provideValues(Contextcontext) { returnImmutableList.of( value(newApple()).withName("apple"), value(newBanana()).withName("banana")); } }
The given
Contextcontains the test class and other annotations on the@TestParameter-annotated parameter/field. This allows more generic providers that take into account custom annotations with extra data, or the implementation of abstract methods on a base test class.
Kotlin
Instead of providing a varargs list of values, you can dynamically provide one as follows:
importcom.google.testing.junit.testparameterinjector.KotlinTestParameters.testValuesIn
@Test
funsendInvalidRequest_fails(
@TestParameter invalidRequest:GetGshoeRequest =
testValuesIn(readRequestsFromFile("invalid_get_gshoe_requests.textproto"))
) { /*...*/ }
privatefunreadRequestsFromFile(filename:String): List<GetGshoeRequest> { /*...*/ }Notes:
The test values are calculated before
@Before[Class]or any@Rules, so don't rely on any state initialized in there.The test values'
toString()will be used for the test names. If you want to customize the value names, you can do that as follows:importcom.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValuesIn @Test funsendInvalidRequest_fails( @TestParameter invalidRequest:GetGshoeRequest = namedTestValuesIn(readRequestsFromFile("invalid_get_gshoe_requests.textproto")) ) { /*...*/ } privatefunreadRequestsFromFile(filename:String): Map<String, GetGshoeRequest> { /*...*/ }
Instead of providing a YAML mapping of parameters, you can implement your own
TestParametersValuesProvider as follows:
importcom.google.testing.junit.testparameterinjector.TestParametersValuesProvider;
importcom.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues;
@Test@TestParameters(valuesProvider = IsAdultValueProvider.class)
publicvoidpersonIsAdult(intage, booleanexpectIsAdult) { ... }
staticfinalclassIsAdultValueProviderextendsTestParametersValuesProvider {
@OverridepublicImmutableList<TestParametersValues> provideValues(Contextcontext) {
returnImmutableList.of(
TestParametersValues.builder()
.name("teenager")
.addParameter("age", 17)
.addParameter("expectIsAdult", false)
.build(),
TestParametersValues.builder()
.name("young adult")
.addParameter("age", 22)
.addParameter("expectIsAdult", true)
.build()
);
}
}Due to JUnit limitations (in both JUnit4 and JUnit5), it is impossible to
use KotlinTestParameters.testValues and friends in a constructor:
// This does *not* workclassMyTest(
@TestParameter valage:Int = testValues(17, 22)
) { /* ... */ }
// Fall back to Java version. This works:classMyTest(
@TestParameter("17", "22") valage:Int
) { /* ... */ }A Robolectric compatible version of TestParameterInjector is available as
RobolectricTestParameterInjector,
which supports all the features of the base RobolectricTestRunner in addition
to the parameterization features of TestParameterInjector.
importorg.robolectric.RobolectricTestParameterInjector;
@RunWith(RobolectricTestParameterInjector.class)
publicclassMyRobolectricTest {
@Testpublicvoidtest(@TestParameterbooleanvalue) { /*...*/ }
}