A TestNG-like dataprovider runner for JUnit having a simplified syntax compared to all the existing JUnit features.
—
This document covers advanced features including custom data provider method resolvers, test name formatting with placeholders, and filtering capabilities.
import com.tngtech.java.junit.dataprovider.DataProviderMethodResolver;
import com.tngtech.java.junit.dataprovider.internal.DefaultDataProviderMethodResolver;
import com.tngtech.java.junit.dataprovider.Placeholders;
import com.tngtech.java.junit.dataprovider.internal.placeholder.BasePlaceholder;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.TestClass;
import java.lang.reflect.Method;
import java.util.List;The DataProviderMethodResolver interface allows you to implement custom logic for finding data provider methods.
public interface DataProviderMethodResolver {
/**
* Returns dataprovider methods for the given test method.
* @param testMethod test method that uses a dataprovider
* @param useDataProvider UseDataProvider annotation on the test method
* @return resolved dataprovider methods or empty list if none found
* @throws IllegalArgumentException if testMethod is null
*/
List<FrameworkMethod> resolve(FrameworkMethod testMethod, UseDataProvider useDataProvider);
}The built-in resolver that implements convention-based method resolution:
public class DefaultDataProviderMethodResolver implements DataProviderMethodResolver {
/**
* Resolve using convention-based strategies in this order:
* 1. Explicit name via UseDataProvider.value()
* 2. Method name equals test method name
* 3. Replace "test" prefix with "dataProvider" or "data"
* 4. Add "dataProvider" or "data" prefix to capitalized method name
* @param testMethod test method that uses a dataprovider
* @param useDataProvider UseDataProvider annotation on the test method
* @return resolved dataprovider methods or empty list if none found
*/
public List<FrameworkMethod> resolve(FrameworkMethod testMethod, UseDataProvider useDataProvider)
/**
* Find classes where data provider methods should be searched.
* @param testMethod test method context
* @param locations specified location classes, or test class if empty
* @return list of TestClass objects to search in
*/
protected List<TestClass> findDataProviderLocations(FrameworkMethod testMethod, Class<?>[] locations)
/**
* Search for data provider methods across multiple locations.
* @param locations TestClass objects to search in
* @param testMethodName name of the test method
* @param value explicit data provider name or DEFAULT_VALUE for convention
* @return list of matching data provider methods
*/
protected List<FrameworkMethod> findDataProviderMethods(List<TestClass> locations, String testMethodName, String value)
/**
* Find data provider method in a specific location using naming conventions.
* @param location TestClass to search in
* @param testMethodName name of the test method
* @param value explicit data provider name or DEFAULT_VALUE for convention
* @return matching data provider method or null if not found
*/
protected FrameworkMethod findDataProviderMethod(TestClass location, String testMethodName, String value)
}Create a custom resolver for specialized naming or lookup strategies:
public class PrefixBasedResolver implements DataProviderMethodResolver {
@Override
public List<FrameworkMethod> resolve(FrameworkMethod testMethod, UseDataProvider useDataProvider) {
String testMethodName = testMethod.getName();
String dataProviderName;
if (!UseDataProvider.DEFAULT_VALUE.equals(useDataProvider.value())) {
dataProviderName = useDataProvider.value();
} else {
// Custom logic: look for methods with "dp_" prefix
dataProviderName = "dp_" + testMethodName;
}
TestClass testClass = new TestClass(testMethod.getMethod().getDeclaringClass());
List<FrameworkMethod> dataProviderMethods = testClass.getAnnotatedMethods(DataProvider.class);
for (FrameworkMethod method : dataProviderMethods) {
if (method.getName().equals(dataProviderName)) {
return Arrays.asList(method);
}
}
return Collections.emptyList();
}
}
// Usage
@Test
@UseDataProvider(resolver = PrefixBasedResolver.class)
public void testStringLength(String input, int expected) {
assertEquals(expected, input.length());
}
@DataProvider
public static Object[][] dp_testStringLength() { // Custom prefix
return new Object[][] { {"hello", 5}, {"world", 5} };
}Use data providers from multiple classes:
public class CommonDataProviders {
@DataProvider
public static Object[][] commonStringData() {
return new Object[][] { {"test", 4}, {"hello", 5} };
}
}
public class SpecialDataProviders {
@DataProvider
public static Object[][] specialStringData() {
return new Object[][] { {"special", 7}, {"unique", 6} };
}
}
@Test
@UseDataProvider(
value = "commonStringData",
location = { CommonDataProviders.class, SpecialDataProviders.class }
)
public void testWithMultipleLocations(String input, int length) {
assertEquals(length, input.length());
}Control how multiple resolvers are used:
@Test
@UseDataProvider(
resolver = { CustomResolver1.class, CustomResolver2.class },
resolveStrategy = ResolveStrategy.UNTIL_FIRST_MATCH // Default
)
public void testFirstMatch(Object data) {
// Uses data from first resolver that finds a match
}
@Test
@UseDataProvider(
resolver = { CustomResolver1.class, CustomResolver2.class },
resolveStrategy = ResolveStrategy.AGGREGATE_ALL_MATCHES
)
public void testAggregated(Object data) {
// Combines data from all resolvers that find matches
}Customize how parameterized test names are displayed using format patterns and placeholders.
The @DataProvider annotation supports format patterns to control test method naming:
@DataProvider(format = "%m[%i: %p[0..-1]]") // Default format
@DataProvider(format = "%m[%i]") // Method name with index only
@DataProvider(format = "%m: %p[0]") // Method name with first parameter
@DataProvider(format = "%c.%m(%p[0..1])") // Class.method(first two params)// Class placeholders
%c // Simple class name (e.g., "DataProviderTest")
%cc // Canonical class name (e.g., "com.example.DataProviderTest")
// Method placeholders
%m // Simple method name (e.g., "testStringLength")
%cm // Complete method signature (e.g., "com.example.Test.testStringLength(java.lang.String)")
// Index placeholder
%i // Data provider index (0, 1, 2, ...)
// Parameter placeholders
%p[0] // First parameter
%p[-1] // Last parameter
%p[1..3] // Parameters 1 through 3
%p[0..-2] // All parameters except last
%p[0..-1] // All parameters@DataProvider(format = "%m[%i: %p[0]]")
public static Object[][] stringLengthData() {
return new Object[][] {
{ "hello", 5 },
{ "world", 5 },
{ "", 0 }
};
}
// Test names: testStringLength[0: hello], testStringLength[1: world], testStringLength[2: ]
@DataProvider(format = "%c.%m: %p[0] -> %p[1]")
public static Object[][] conversionData() {
return new Object[][] {
{ "123", 123 },
{ "true", true }
};
}
// Test names: MyTest.testConversion: 123 -> 123, MyTest.testConversion: true -> true
@DataProvider(format = "%m(%p[0..1]) #%i")
public static Object[][] rangeData() {
return new Object[][] {
{ 1, 10, "range1" },
{ 20, 30, "range2" }
};
}
// Test names: testRange(1, 10) #0, testRange(20, 30) #1Extend the placeholder system with custom formatting logic.
The Placeholders class manages the collection of placeholder implementations used for test method name formatting.
public class Placeholders {
/**
* Get all registered placeholders. Returns modifiable list.
* @return all BasePlaceholder instances for format processing
*/
public static List<BasePlaceholder> all()
/**
* Reset to default placeholders. Clears all custom placeholders and
* restores the default set: %c, %cc, %m, %cm, %i, %p[x]
*/
public static void reset()
}The default placeholders included are:
Implement BasePlaceholder for custom formatting:
public abstract class BasePlaceholder {
/**
* Constructor requires placeholder regex pattern.
* @param placeholderRegex regular expression to match placeholder
*/
public BasePlaceholder(String placeholderRegex)
/**
* Set context for placeholder processing.
* @param method test method being processed
* @param idx data provider row index
* @param parameters test method parameters
*/
public void setContext(Method method, int idx, Object[] parameters)
/**
* Process format string, replacing placeholder occurrences.
* @param formatPattern input format pattern
* @return processed format string with replacements
*/
public String process(String formatPattern)
/**
* Generate replacement for found placeholder. Override this method.
* @param placeholder matched placeholder string
* @return replacement string for the placeholder
*/
protected abstract String getReplacementFor(String placeholder);
}Example custom placeholder:
public class TimestampPlaceholder extends BasePlaceholder {
private String timestamp;
public TimestampPlaceholder() {
super("%t"); // Regex pattern to match %t placeholder
}
@Override
public void setContext(Method method, int idx, Object[] parameters) {
this.timestamp = Instant.now().toString();
}
@Override
protected String getReplacementFor(String placeholder) {
return timestamp;
}
}
// Register custom placeholder
static {
Placeholders.all().add(0, new TimestampPlaceholder());
}
@DataProvider(format = "%m[%i] at %t")
public static Object[][] timestampedData() {
return new Object[][] { {"test1"}, {"test2"} };
}
// Test names: testMethod[0] at 2023-12-01T10:30:00Z, testMethod[1] at 2023-12-01T10:30:00Zpublic class HashPlaceholder extends BasePlaceholder {
private Object[] parameters;
public HashPlaceholder() {
super("%hash"); // Regex pattern to match %hash placeholder
}
@Override
public void setContext(Method method, int idx, Object[] parameters) {
this.parameters = parameters;
}
@Override
protected String getReplacementFor(String placeholder) {
return String.valueOf(Arrays.hashCode(parameters));
}
}
@DataProvider(format = "%m[%hash]")
public static Object[][] hashedData() {
return new Object[][] {
{"param1", "param2"},
{"param3", "param4"}
};
}
// Test names: testMethod[12345], testMethod[67890] (hash values)Control which tests run using JUnit filters enhanced for data provider support.
public class DataProviderFilter extends Filter {
/**
* Create filter that supports data provider row filtering.
* @param filter original filter to wrap
*/
public DataProviderFilter(Filter filter)
/**
* Determine if test should run, supporting data provider syntax.
* @param description test description
* @return true if test should run
*/
public boolean shouldRun(Description description)
/**
* Get description of wrapped filter.
* @return filter description
*/
public String describe()
// Pattern constants for parsing test descriptions
static final Pattern DESCRIPTION_PATTERN;
static final Pattern GENEROUS_DESCRIPTION_PATTERN;
}The DataProviderRunner automatically wraps filters to support data provider test selection:
// These filtering patterns work with DataProviderFilter:
// "testMethod" - runs all data provider rows for testMethod
// "testMethod[0]" - runs only row 0 of testMethod data provider
// "testMethod[0: param1, param2]" - runs specific parameterized test
// "com.example.TestClass" - runs all tests in TestClass@RunWith(DataProviderRunner.class)
public class FilteredTest {
@DataProvider
public static Object[][] allData() {
return new Object[][] {
{"test1", 1}, {"test2", 2}, {"test3", 3}
};
}
@Test
@UseDataProvider("allData")
public void testFiltered(String name, int value) {
// Only specific rows will run based on filter
}
}
// Run only second data row:
// mvn test -Dtest="FilteredTest#testFiltered[1]*"Integrate with custom JUnit filters:
public class CustomTestFilter extends Filter {
@Override
public boolean shouldRun(Description description) {
// Custom filtering logic
return description.getDisplayName().contains("important");
}
@Override
public String describe() {
return "Custom filter for important tests";
}
}
// DataProviderRunner automatically wraps custom filters:
runner.filter(new CustomTestFilter());
// Becomes: runner.filter(new DataProviderFilter(new CustomTestFilter()));Data provider tests work seamlessly with JUnit Categories:
public interface SlowTests {}
public interface FastTests {}
@RunWith(DataProviderRunner.class)
@Category({FastTests.class})
public class CategorizedTest {
@DataProvider
public static Object[][] quickData() {
return new Object[][] { {"quick1"}, {"quick2"} };
}
@Test
@UseDataProvider("quickData")
@Category(SlowTests.class) // Override class-level category
public void testCategorized(String input) {
// Test logic
}
}Extend DataProviderRunner for specialized behavior:
public class CustomDataProviderRunner extends DataProviderRunner {
public CustomDataProviderRunner(Class<?> clazz) throws InitializationError {
super(clazz);
}
@Override
protected void initializeHelpers() {
super.initializeHelpers();
// Customize data converter, test generator, or validator
this.dataConverter = new CustomDataConverter();
}
@Override
protected List<FrameworkMethod> computeTestMethods() {
List<FrameworkMethod> methods = super.computeTestMethods();
// Apply custom filtering or transformation
return methods.stream()
.filter(this::shouldIncludeMethod)
.collect(Collectors.toList());
}
private boolean shouldIncludeMethod(FrameworkMethod method) {
// Custom inclusion logic
return !method.getName().startsWith("skip");
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-tngtech-java--junit-dataprovider