Skip to content

Commit

Permalink
added splitBy, convertNulls and trimValues parameter to @dataProvider (
Browse files Browse the repository at this point in the history
  • Loading branch information
aaschmid committed Jun 28, 2014
1 parent ab0dcfd commit a446326
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 151 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ class DataProviderTest {

Instead of using ```@UseDataProvider``` to point to a method providing test data, you can directly
pass test data using ```@DataProvider``` annotation and its ```#value()``` method to provide an
array of comma-separated ```String```s. Each comma-separated ```String``` is split and trimmed back by
array of regex-separated ```String```s. Each regex-separated ```String``` is split and trimmed back by
spaces (= "``` ```"), tabs (= "```\t```) and line-separator (= "```\n```" or "```\r```"). The
resulting ```String``` is then parsed to its corresponding type in the test method signature. All primitive
types (e.g. ```char```, ```boolean```, ```int```), primitive wrapper types (e.g. ```Long```, ```Double```), ```Enum```s,
Expand Down Expand Up @@ -325,6 +325,7 @@ Release notes

### tbd. (???)

* added ```splitBy```, ```convertNulls``` and ```trimValues```` parameter to ```@DataProvider``` ([#24](/../../issues/24))
* ...

### v1.7.0 (20-Jun-2014)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;

/**
* Mark a method as a data provider used by a test method or use it directly at the test method and provide data via
* {@link #value()} attribute.
* <ul>
* <li><i>Use it on a separate method:</i> The name of the data provider is the the name of the method. The method must
* be static and return an {@link Object} {@code [][]}. The test method will be called with each "row" of this
* two-dimensional array. The test method must be annotated with {@code @}{@link UseDataProvider}. This annotation
* behaves pretty much the same as the {@code @DataProvider} annotation from <a href="http://testng.org/">TestNG</a>.
* be static and return an {@link Object}{@code [][]}, a {@link List}{@code <List<Object>>}, or a {@link String}
* {@code []}. The test method will be called with each "row" of this two-dimensional array. The test method must be
* annotated with {@code @}{@link UseDataProvider}. This annotation behaves pretty much the same as the
* {@code @DataProvider} annotation from <a href="http://testng.org/">TestNG</a>.
* <p>
* <b>Note:</b> The name of the test method in the JUnit result will be the name of the test method (annotated by
* {@code @}{@link UseDataProvider}) suffixed by the parameters. The last parameter is assumed to be the expected value
* and will not be printed.</li>
* {@code @}{@link UseDataProvider}) suffixed by the parameters.</li>
* <li>
* <p>
* <i>Use it directly on test method:</i> Provide all the data for the test method parameters as comma-separated
* <i>Use it directly on test method:</i> Provide all the data for the test method parameters as regex-separated
* {@link String}s using {@code String[] value()}.
* <p>
* <b>Note:</b> All parameters of the test method must be primitive types (e.g. {@code char}, {@code int},
Expand All @@ -31,13 +32,37 @@
* {@link String} must not contain commas! The {@link String} "null" will always be passed as {@code null}.</li>
* </ul>
* <p>
* Copyright by TNG Technology Consulting GmbH, Germany
* If the test method arguments are retrieved from a regex-separated {@link String}{@code []}, the additional annotation
* parameters can be used to customized the generation/conversion behavior.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataProvider {

/** Use this to define a list of parameters each as a comma-separated {@link String} for the test method. */
/** Define a list of parameters each as a regex-separated {@link String} for the annotated test method. */
String[] value() default {};

/**
* The delimiting regular expression by which the regex-separated {@link String}s given by {@link #value()} or
* returned by the method annotated with {@code @}{@link DataProvider} are split.
*
* @see String#split(String)
*/
String splitBy() default ",";

/**
* Determines if every "null"-{@link String} in {@link #value()} or returned by the method annotated with {@code @}
* {@link DataProvider} should be converted to {@code null} (= {@code true} ) or used as {@link String} (=
* {@code false}).
*/
boolean convertNulls() default true;

/**
* {@code true} if leading and trailing whitespace should be omitted in split {@link String}s given by
* {@link #value()} or returned by the method annotated with {@code @}{@link DataProvider}, {@code false} otherwise.
*
* @see String#trim()
*/
boolean trimValues() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ protected void validateTestMethods(List<Throwable> errors) {
errors.add(new Exception("No such data provider: "
+ testMethod.getAnnotation(UseDataProvider.class).value()));
} else {
testValidator.validateDataProviderMethod(dataProviderMethod, errors);
DataProvider dataProvider = dataProviderMethod.getAnnotation(DataProvider.class);
if (dataProvider == null) {
throw new IllegalStateException(String.format("@%s annotaion not found on data provider method %s",
DataProvider.class.getSimpleName(), dataProviderMethod.getName()));
}
testValidator.validateDataProviderMethod(dataProviderMethod, dataProvider, errors);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import java.lang.annotation.Target;

/**
* Mark a test method for use with a data provider. The value must be the name of a {@code @}{@link DataProvider}
* method.
* Annotate a test method for using it with a data provider. The {@link #value()} must be the name of a {@code @}
* {@link DataProvider} method which can optionally be located in another class (see {@link #location()}).
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -18,6 +18,10 @@
/** The required name of the data provider method to use test data from. */
String value();

/** The class holding the data provider method, defaults to the test class (just first class will be considered). */
/**
* Optionally specify the class holding the data provider method having the name given in {@link #value()}. Defaults
* to the test class where {@code @}{@link UseDataProvider} annotation is used. (Just first class will be
* considered).
*/
Class<?>[] location() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,28 @@
import java.util.ArrayList;
import java.util.List;

import com.tngtech.java.junit.dataprovider.DataProvider;

/**
* Internal class to convert some data to its corresponding parameters.
*/
public class DataConverter {

public static class Settings {
public final String splitBy;
public final boolean convertNulls;
public final boolean trimValues;

public Settings(DataProvider dataProvider) {
if (dataProvider == null) {
throw new NullPointerException("dataProvider must not be null");
}
this.splitBy = dataProvider.splitBy();
this.convertNulls = dataProvider.convertNulls();
this.trimValues = dataProvider.trimValues();
}
}

/**
* Returns {@code true} iif this {@link DataConverter} can convert the given {@code type}. Currently supported
* {@code type}s:
Expand Down Expand Up @@ -52,15 +69,19 @@ public boolean canConvert(Type type) {
*
* @param data to be converted
* @param parameterTypes required types for {@code data}
* @param settings to be used to convert given {@code data}
* @return converted data as {@link List}{@code <}{@link Object}{@code []>} with the required {@code parameterTypes}
* @throws NullPointerException iif given {@code parameterTypes} is {@code null}
* @throws NullPointerException iif given {@code parameterTypes} or {@code settings} are {@code null}
* @throws IllegalArgumentException iif given {@code parameterTypes} is empty
* @throws ClassCastException iif {@code data} is not a compatible type
*/
public List<Object[]> convert(Object data, Class<?>[] parameterTypes) {
public List<Object[]> convert(Object data, Class<?>[] parameterTypes, Settings settings) {
if (parameterTypes == null) {
throw new NullPointerException("parameterTypes must not be null");
}
if (settings == null) {
throw new NullPointerException("settings must not be null");
}
if (parameterTypes.length == 0) {
throw new IllegalArgumentException("parameterTypes must not be empty");
}
Expand All @@ -76,7 +97,7 @@ public List<Object[]> convert(Object data, Class<?>[] parameterTypes) {
} else if (data instanceof String[]) {
int idx = 0;
for (String paramString : (String[]) data) {
result.add(getParametersFor(paramString, parameterTypes, idx++));
result.add(getParametersFor(paramString, parameterTypes, settings, idx++));
}
return result;

Expand All @@ -97,89 +118,106 @@ public List<Object[]> convert(Object data, Class<?>[] parameterTypes) {
* This method is package private (= visible) for testing.
* </p>
*
* @param data comma separated {@link String} of parameters for test method
* @param parameterTypes target types of parameters to which corresponding value in comma separated {@code data}
* @param data regex-separated {@link String} of parameters for test method
* @param parameterTypes target types of parameters to which corresponding value in regex-separated {@code data}
* should be converted
* @param settings to be used to convert given {@code data}
* @param rowIdx index of current {@code data} for better error messages
* @return split, trimmed and converted {@code Object[]} of supplied comma separated {@code data}
* @return split, trimmed and converted {@code Object[]} of supplied regex-separated {@code data}
*/
Object[] getParametersFor(String data, Class<?>[] parameterTypes, int rowIdx) {
Object[] getParametersFor(String data, Class<?>[] parameterTypes, Settings settings, int rowIdx) {
if (data == null) {
return new Object[] { null };
}

Object[] result = new Object[parameterTypes.length];

String[] splitData = (data + " ").split(","); // add trailing whitespace that split for comma ended data works
String[] splitData = splitBy(data, settings.splitBy);
if (parameterTypes.length != splitData.length) {
throw new Error(String.format("Test method expected %d parameters but got %d from @DataProvider row %d",
parameterTypes.length, splitData.length, rowIdx));
}

for (int idx = 0; idx < splitData.length; idx++) {
result[idx] = convertValue(splitData[idx].trim(), parameterTypes[idx]);
result[idx] = convertValue(splitData[idx], parameterTypes[idx], settings);
}
return result;
}

private Object convertValue(String str, Class<?> targetType) {
if ("null".equals(str)) {
private String[] splitBy(String data, String regex) {
// add trailing null terminator that split for "regex" ending data works properly
String[] splitData = (data + "\0").split(regex);

// remove added null terminator
int lastItemIdx = splitData.length - 1;
splitData[lastItemIdx] = splitData[lastItemIdx].substring(0, splitData[lastItemIdx].length() - 1);

return splitData;
}

private Object convertValue(String str, Class<?> targetType, Settings settings) {
String toConvert = (settings.trimValues) ? str.trim() : str;

if (settings.convertNulls && "null".equals(toConvert)) {
return null;
}

if (String.class.equals(targetType)) {
return str;
return toConvert;
}

if (boolean.class.equals(targetType) || Boolean.class.equals(targetType)) {
return Boolean.valueOf(str);
return Boolean.valueOf(toConvert);
}
if (byte.class.equals(targetType) || Byte.class.equals(targetType)) {
return Byte.valueOf(str);
return Byte.valueOf(toConvert);
}
if (char.class.equals(targetType) || Character.class.equals(targetType)) {
if (str.length() == 1) {
return str.charAt(0);
if (toConvert.length() == 1) {
return toConvert.charAt(0);
}
throw new Error(String.format("'%s' cannot be converted to %s.", str, targetType.getSimpleName()));
throw new Error(String.format("'%s' cannot be converted to %s.", toConvert, targetType.getSimpleName()));
}
if (short.class.equals(targetType) || Short.class.equals(targetType)) {
return Short.valueOf(str);
return Short.valueOf(toConvert);
}
if (int.class.equals(targetType) || Integer.class.equals(targetType)) {
return Integer.valueOf(str);
return Integer.valueOf(toConvert);
}
if (long.class.equals(targetType) || Long.class.equals(targetType)) {
String longStr = str;
String longStr = toConvert;
if (longStr.endsWith("l")) {
longStr = longStr.substring(0, longStr.length() - 1);
}
return Long.valueOf(longStr);
}
if (float.class.equals(targetType) || Float.class.equals(targetType)) {
return Float.valueOf(str);
return Float.valueOf(toConvert);
}
if (double.class.equals(targetType) || Double.class.equals(targetType)) {
return Double.valueOf(str);
return Double.valueOf(toConvert);
}

if (targetType.isEnum()) {
try {
@SuppressWarnings({ "rawtypes", "unchecked" })
Enum result = Enum.valueOf((Class<Enum>) targetType, str);
Enum result = Enum.valueOf((Class<Enum>) targetType, toConvert);
return result;

} catch (IllegalArgumentException e) {
throw new Error(String.format(
"'%s' is not a valid value of enum %s. Please be aware of case sensitivity.", str,
"'%s' is not a valid value of enum %s. Please be aware of case sensitivity.", toConvert,
targetType.getSimpleName()));
}
}

for (Constructor<?> constructor : targetType.getConstructors()) {
if (constructor.getParameterTypes().length == 1 && String.class.equals(constructor.getParameterTypes()[0])) {
try {
return constructor.newInstance(str);
return constructor.newInstance(toConvert);

} catch (Exception e) {
throw new Error(String.format("Tried to invoke '%s' for argument '%s'. Exception: %s", constructor,
str, e.getMessage()), e);
toConvert, e.getMessage()), e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class ParametersFormatter {
* </tr>
* </table>
*
* @param parameters the parameters are converted to a comma separated {@link String}
* @param parameters the parameters are converted to a regex-separated {@link String}
* @return a {@link String} representation of the given parameters
*/
public String format(Object[] parameters) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderFrameworkMethod;
import com.tngtech.java.junit.dataprovider.internal.DataConverter.Settings;

public class TestGenerator {

Expand Down Expand Up @@ -60,17 +61,20 @@ public List<FrameworkMethod> generateExplodedTestMethodsFor(FrameworkMethod test
* @return a list of methods, each method bound to a parameter combination returned by the data provider
*/
List<FrameworkMethod> explodeTestMethod(FrameworkMethod testMethod, FrameworkMethod dataProviderMethod) {
Object dataProviderParameters;
Object data;
try {
dataProviderParameters = dataProviderMethod.invokeExplosively(null);
data = dataProviderMethod.invokeExplosively(null);
} catch (Throwable t) {
throw new Error(String.format("Exception while invoking data provider method '%s': %s",
dataProviderMethod.getName(), t.getMessage()), t);
}

Settings settings = new Settings(dataProviderMethod.getAnnotation(DataProvider.class));

List<Object[]> converted = dataConverter.convert(data, testMethod.getMethod().getParameterTypes(), settings);
String emptyResultMessage = String.format("Data provider '%s' must neither be null nor empty but was: %s.",
dataProviderMethod.getName(), dataProviderParameters);
return explodeTestMethod(testMethod, dataProviderParameters, emptyResultMessage);
dataProviderMethod.getName(), data);
return explodeTestMethod(testMethod, converted, emptyResultMessage);
}

/**
Expand All @@ -84,17 +88,21 @@ List<FrameworkMethod> explodeTestMethod(FrameworkMethod testMethod, FrameworkMet
* @return a list of methods, each method bound to a parameter combination returned by the {@link DataProvider}
*/
List<FrameworkMethod> explodeTestMethod(FrameworkMethod testMethod, DataProvider dataProvider) {
String[] dataProviderParameters = dataProvider.value();
String[] data = dataProvider.value();

List<Object[]> converted = dataConverter.convert(data, testMethod.getMethod().getParameterTypes(),
new Settings(dataProvider));
String emptyResultMessage = String.format("%s.value() must be set but was: %s.", dataProvider.getClass()
.getSimpleName(), Arrays.toString(dataProviderParameters));
return explodeTestMethod(testMethod, dataProviderParameters, emptyResultMessage);
.getSimpleName(), Arrays.toString(data));
return explodeTestMethod(testMethod, converted, emptyResultMessage);
}

private List<FrameworkMethod> explodeTestMethod(FrameworkMethod testMethod, Object data, String emptyResultMessage) {
private List<FrameworkMethod> explodeTestMethod(FrameworkMethod testMethod, List<Object[]> converted,
String emptyResultMessage) {

int idx = 0;
List<FrameworkMethod> result = new ArrayList<FrameworkMethod>();
for (Object[] parameters : dataConverter.convert(data, testMethod.getMethod().getParameterTypes())) {
for (Object[] parameters : converted) {
result.add(new DataProviderFrameworkMethod(testMethod.getMethod(), idx++, parameters));
}
if (result.isEmpty()) {
Expand Down
Loading

0 comments on commit a446326

Please sign in to comment.