makigas - test en java
Cambio de nombres de pruebas parametrizadas (12)
¿Hay alguna manera de establecer mis propios nombres de caso de prueba personalizados cuando se utilizan pruebas parametrizadas en JUnit4?
Me gustaría cambiar el valor predeterminado - [Test class].runTest[n]
- a algo significativo.
Con Parameterized
como modelo, escribí mi propio runner / suite de prueba personalizado, solo me tomó alrededor de media hora. Es ligeramente diferente de la etiqueta etiquetada de Darrenp, LabelledParameterized
que le permite especificar un nombre explícitamente en lugar de confiar en el primer parámetro toString()
.
Tampoco usa matrices porque odio las matrices. :)
public class PolySuite extends Suite {
// //////////////////////////////
// Public helper interfaces
/**
* Annotation for a method which returns a {@link Configuration}
* to be injected into the test class constructor
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public static @interface Config {
}
public static interface Configuration {
int size();
Object getTestValue(int index);
String getTestName(int index);
}
// //////////////////////////////
// Fields
private final List<Runner> runners;
// //////////////////////////////
// Constructor
/**
* Only called reflectively. Do not use programmatically.
* @param c the test class
* @throws Throwable if something bad happens
*/
public PolySuite(Class<?> c) throws Throwable {
super(c, Collections.<Runner>emptyList());
TestClass testClass = getTestClass();
Class<?> jTestClass = testClass.getJavaClass();
Configuration configuration = getConfiguration(testClass);
List<Runner> runners = new ArrayList<Runner>();
for (int i = 0, size = configuration.size(); i < size; i++) {
SingleRunner runner = new SingleRunner(jTestClass, configuration.getTestValue(i), configuration.getTestName(i));
runners.add(runner);
}
this.runners = runners;
}
// //////////////////////////////
// Overrides
@Override
protected List<Runner> getChildren() {
return runners;
}
// //////////////////////////////
// Private
private Configuration getConfiguration(TestClass testClass) throws Throwable {
return (Configuration) getConfigMethod(testClass).invokeExplosively(null);
}
private FrameworkMethod getConfigMethod(TestClass testClass) {
List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Config.class);
if (methods.isEmpty()) {
throw new IllegalStateException("@" + Config.class.getSimpleName() + " method not found");
}
if (methods.size() > 1) {
throw new IllegalStateException("Too many @" + Config.class.getSimpleName() + " methods");
}
FrameworkMethod method = methods.get(0);
int modifiers = method.getMethod().getModifiers();
if (!(Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
throw new IllegalStateException("@" + Config.class.getSimpleName() + " method /"" + method.getName() + "/" must be public static");
}
return method;
}
// //////////////////////////////
// Helper classes
private static class SingleRunner extends BlockJUnit4ClassRunner {
private final Object testVal;
private final String testName;
SingleRunner(Class<?> testClass, Object testVal, String testName) throws InitializationError {
super(testClass);
this.testVal = testVal;
this.testName = testName;
}
@Override
protected Object createTest() throws Exception {
return getTestClass().getOnlyConstructor().newInstance(testVal);
}
@Override
protected String getName() {
return testName;
}
@Override
protected String testName(FrameworkMethod method) {
return testName + ": " + method.getName();
}
@Override
protected void validateConstructor(List<Throwable> errors) {
validateOnlyOneConstructor(errors);
}
@Override
protected Statement classBlock(RunNotifier notifier) {
return childrenInvoker(notifier);
}
}
}
Y un ejemplo:
@RunWith(PolySuite.class)
public class PolySuiteExample {
// //////////////////////////////
// Fixture
@Config
public static Configuration getConfig() {
return new Configuration() {
@Override
public int size() {
return 10;
}
@Override
public Integer getTestValue(int index) {
return index * 2;
}
@Override
public String getTestName(int index) {
return "test" + index;
}
};
}
// //////////////////////////////
// Fields
private final int testVal;
// //////////////////////////////
// Constructor
public PolySuiteExample(int testVal) {
this.testVal = testVal;
}
// //////////////////////////////
// Test
@Ignore
@Test
public void odd() {
assertFalse(testVal % 2 == 0);
}
@Test
public void even() {
assertTrue(testVal % 2 == 0);
}
}
Dado que el parámetro al que se accede (por ejemplo, con "{0}"
siempre devuelve la representación de toString()
, una solución alternativa sería realizar una implementación anónima y anular toString()
en cada caso. Por ejemplo:
public static Iterable<? extends Object> data() {
return Arrays.asList(
new MyObject(myParams...) {public String toString(){return "my custom test name";}},
new MyObject(myParams...) {public String toString(){return "my other custom test name";}},
//etc...
);
}
Echa un vistazo a JUnitParams como se mencionó en dsaff, funciona usando ant para crear descripciones de métodos de prueba parametrizadas en el informe html.
Esto fue después de probar LabelledParameterized y encontrar que, aunque funciona con eclipse, no funciona con hormiga en lo que respecta al informe html.
Aclamaciones,
En cuanto a JUnit 4.5, su corredor claramente no lo admite, ya que esa lógica está enterrada dentro de una clase privada dentro de la clase Parametrizada. No podría usar el corredor JUnit Parameterized, y crear el suyo propio que comprendería el concepto de nombres (lo que lleva a la pregunta de cómo puede establecer un nombre ...).
Desde la perspectiva de JUnit, sería bueno si en lugar de (o además de) solo pasasen un incremento, pasarían los argumentos delimitados por comas. TestNG hace esto. Si la característica es importante para usted, puede hacer un comentario en la lista de correo de yahoo a la que se hace referencia en www.junit.org.
Hago un uso extensivo de la importación estática para Assert y amigos, por lo que es fácil para mí redefinir la aserción:
private <T> void assertThat(final T actual, final Matcher<T> expected) {
Assert.assertThat(editThisToDisplaySomethingForYourDatum, actual, expected);
}
Por ejemplo, podría agregar un campo de "nombre" a su clase de prueba, inicializado en el constructor y mostrarlo en caso de falla de prueba. Solo pásalo como los primeros elementos de tu matriz de parámetros para cada prueba. Esto también ayuda a etiquetar los datos:
public ExampleTest(final String testLabel, final int one, final int two) {
this.testLabel = testLabel;
// ...
}
@Parameters
public static Collection<Object[]> data() {
return asList(new Object[][]{
{"first test", 3, 4},
{"second test", 5, 6}
});
}
Nada de eso me funcionaba, así que obtuve la fuente de Parametrizado y lo modifiqué para crear un nuevo corredor de prueba. ¡No tuve que cambiar mucho, pero FUNCIONA!
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.Assert;
import org.junit.internal.runners.ClassRoadie;
import org.junit.internal.runners.CompositeRunner;
import org.junit.internal.runners.InitializationError;
import org.junit.internal.runners.JUnit4ClassRunner;
import org.junit.internal.runners.MethodValidator;
import org.junit.internal.runners.TestClass;
import org.junit.runner.notification.RunNotifier;
public class LabelledParameterized extends CompositeRunner {
static class TestClassRunnerForParameters extends JUnit4ClassRunner {
private final Object[] fParameters;
private final String fParameterFirstValue;
private final Constructor<?> fConstructor;
TestClassRunnerForParameters(TestClass testClass, Object[] parameters, int i) throws InitializationError {
super(testClass.getJavaClass()); // todo
fParameters = parameters;
if (parameters != null) {
fParameterFirstValue = Arrays.asList(parameters).toString();
} else {
fParameterFirstValue = String.valueOf(i);
}
fConstructor = getOnlyConstructor();
}
@Override
protected Object createTest() throws Exception {
return fConstructor.newInstance(fParameters);
}
@Override
protected String getName() {
return String.format("%s", fParameterFirstValue);
}
@Override
protected String testName(final Method method) {
return String.format("%s%s", method.getName(), fParameterFirstValue);
}
private Constructor<?> getOnlyConstructor() {
Constructor<?>[] constructors = getTestClass().getJavaClass().getConstructors();
Assert.assertEquals(1, constructors.length);
return constructors[0];
}
@Override
protected void validate() throws InitializationError {
// do nothing: validated before.
}
@Override
public void run(RunNotifier notifier) {
runMethods(notifier);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public static @interface Parameters {
}
private final TestClass fTestClass;
public LabelledParameterized(Class<?> klass) throws Exception {
super(klass.getName());
fTestClass = new TestClass(klass);
MethodValidator methodValidator = new MethodValidator(fTestClass);
methodValidator.validateStaticMethods();
methodValidator.validateInstanceMethods();
methodValidator.assertValid();
int i = 0;
for (final Object each : getParametersList()) {
if (each instanceof Object[])
add(new TestClassRunnerForParameters(fTestClass, (Object[]) each, i++));
else
throw new Exception(String.format("%s.%s() must return a Collection of arrays.", fTestClass.getName(), getParametersMethod().getName()));
}
}
@Override
public void run(final RunNotifier notifier) {
new ClassRoadie(notifier, fTestClass, getDescription(), new Runnable() {
public void run() {
runChildren(notifier);
}
}).runProtected();
}
private Collection<?> getParametersList() throws IllegalAccessException, InvocationTargetException, Exception {
return (Collection<?>) getParametersMethod().invoke(null);
}
private Method getParametersMethod() throws Exception {
List<Method> methods = fTestClass.getAnnotatedMethods(Parameters.class);
for (Method each : methods) {
int modifiers = each.getModifiers();
if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))
return each;
}
throw new Exception("No public static parameters method on class " + getName());
}
public static Collection<Object[]> eachOne(Object... params) {
List<Object[]> results = new ArrayList<Object[]>();
for (Object param : params)
results.add(new Object[] { param });
return results;
}
}
Puedes crear un método como
@Test
public void name() {
Assert.assertEquals("", inboundFileName);
}
Si bien no lo usaría todo el tiempo, sería útil averiguar exactamente qué prueba es el número 143.
Recientemente me encontré con el mismo problema cuando uso JUnit 4.3.1. Implementé una nueva clase que amplía Parametricized llamado LabelledParameterized. Ha sido probado usando JUnit 4.3.1, 4.4 y 4.5. Reconstruye la instancia de Descripción utilizando la representación de Cadena del primer argumento de cada matriz de parámetros del método @Parameters. Puedes ver el código para esto en:
http://code.google.com/p/migen/source/browse/trunk/java/src/.../LabelledParameterized.java?r=3789
y un ejemplo de su uso en:
http://code.google.com/p/migen/source/browse/trunk/java/src/.../ServerBuilderTest.java?r=3789
¡La descripción de la prueba formatea muy bien en Eclipse, que es lo que quería, ya que esto hace que las pruebas fallidas sean mucho más fáciles de encontrar! Probablemente refine y documente las clases en los próximos días / semanas. Suelta el ''?'' parte de las URL si quieres la vanguardia. :-)
Para usarlo, todo lo que tiene que hacer es copiar esa clase (GPL v3) y cambiar @RunWith (Parameterized.class) a @RunWith (LabelledParameterized.class) suponiendo que el primer elemento de su lista de parámetros es una etiqueta sensible.
No sé si los lanzamientos posteriores de JUnit abordan este problema, pero incluso si lo hicieran, no puedo actualizar JUnit ya que todos mis codesarrolladores tendrían que actualizar también y tenemos prioridades más altas que el re-tooling. De ahí que el trabajo en la clase sea compilable por versiones múltiples de JUnit.
Nota: hay una reflexión jiggery-pokery para que se ejecute en las diferentes versiones de JUnit como se indica más arriba. La versión específicamente para JUnit 4.3.1 se puede encontrar here y, para JUnit 4.4 y 4.5, here .
También puede probar JUnitParams: http://code.google.com/p/junitparams/
Una solución alternativa sería atrapar y anidar todos los Throwables en un nuevo Throwable con un mensaje personalizado que contenga toda la información sobre los parámetros. El mensaje aparecería en el seguimiento de la pila. Esto funciona siempre que una prueba falla para todas las aserciones, errores y excepciones ya que todas son subclases de Throwable.
Mi código se ve así:
@RunWith(Parameterized.class)
public class ParameterizedTest {
int parameter;
public ParameterizedTest(int parameter) {
super();
this.parameter = parameter;
}
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] { {1}, {2} });
}
@Test
public void test() throws Throwable {
try {
assertTrue(parameter%2==0);
}
catch(Throwable thrown) {
throw new Throwable("parameter="+parameter, thrown);
}
}
}
El seguimiento de la pila de la prueba fallida es:
java.lang.Throwable: parameter=1
at sample.ParameterizedTest.test(ParameterizedTest.java:34)
Caused by: java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:92)
at org.junit.Assert.assertTrue(Assert.java:43)
at org.junit.Assert.assertTrue(Assert.java:54)
at sample.ParameterizedTest.test(ParameterizedTest.java:31)
... 31 more
desde junit4.8.2, puede crear su propia clase MyParameterized simplemente copie la clase Parameterized. cambie los métodos getName () y testName () en TestClassRunnerForParameters.
Esta característica la ha convertido en JUnit 4.11 .
Para usar cambie el nombre de las pruebas parametrizadas, diga:
@Parameters(name="namestring")
namestring
es una cadena que puede tener los siguientes marcadores de posición especiales:
-
{index}
- el índice de este conjunto de argumentos. Lanamestring
predeterminada es{index}
. -
{0}
- el primer valor de parámetro de esta invocación de la prueba. -
{1}
- el segundo valor del parámetro - y así
El nombre final de la prueba será el nombre del método de prueba, seguido de la namestring
de namestring
entre paréntesis, como se muestra a continuación.
Por ejemplo (adaptado de la prueba unitaria para la anotación Parameterized
):
@RunWith(Parameterized.class)
static public class FibonacciTest {
@Parameters( name = "{index}: fib({0})={1}" )
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 },
{ 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } });
}
private final int fInput;
private final int fExpected;
public FibonacciTest(int input, int expected) {
fInput= input;
fExpected= expected;
}
@Test
public void testFib() {
assertEquals(fExpected, fib(fInput));
}
private int fib(int x) {
// TODO: actually calculate Fibonacci numbers
return 0;
}
}
dará nombres como testFib[1: fib(1)=1]
y testFib[4: fib(4)=3]
. (La parte testFib
del nombre es el nombre del método de @Test
).