修改参数化测试的名称

226

在使用JUnit4中的参数化测试时,是否有一种方法可以设置自己的自定义测试用例名称?

我想将默认名称—[Test class].runTest[n]—更改为有意义的内容。

15个回答

336

这个功能已经被包含在JUnit 4.11中。

要更改参数化测试的名称,您可以这样说:

@Parameters(name="namestring")

namestring 是一个字符串,它可以有以下特殊占位符:

  • {index} - 这组参数的索引。默认的namestring{index}
  • {0} - 此次测试调用的第一个参数值。
  • {1} - 第二个参数值
  • 以此类推

测试的最终名称将是测试方法的名称,后面跟着括号中的namestring,如下所示。

例如(从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;
    }
}

会产生类似于testFib[1: fib(1)=1]testFib[4: fib(4)=3]这样的名称。(名称中的testFib部分是@Test方法的方法名)。


4
它应该会出现在4.11版本中,它已经在主分支中了。现在什么时候会推出4.11版本,这是一个好问题 :-) - Matthew Farwell
1
4.11现在处于beta版本,可以从上面相同的链接下载 :-) - rescdsk
2
是的,但是有一个错误。如果您在参数“name”的值中放置括号,就像您在此帖子中所做的那样,它会破坏Eclipse中单元测试名称的显示。 - djangofan
11
如果{0}{1}是数组,那该怎么办呢?JUnit最好调用Arrays.toString({0})而不是{0}.toString()。例如,我的 data() 方法返回 Arrays.asList(new Object[][] {{ new int[] { 1, 3, 2 }, new int[] { 1, 2, 3 } }}); - dogbane
1
@djangofan 这是一个8年前的Eclipse bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=102512 - Pool
显示剩余10条评论

39

查看JUnit 4.5,它的运行程序明显不支持此功能,因为该逻辑被嵌入Parameterized类中的私有类中。您可能无法使用JUnit Parameterized运行程序,而需要创建自己的运行程序,它可以理解名称的概念(这引出了一个问题,如何设置名称...)。

从JUnit的角度来看,如果不仅仅传递增量,而是传递逗号分隔的参数,那将是很好的。TestNG就是这样做的。如果您认为该功能很重要,可以在www.junit.org上引用的雅虎邮件列表上发表评论。


4
如果JUnit能有这方面的改进,我会非常感激! - guerda
18
刚刚查看了一下,在这个网址http://github.com/KentBeck/junit/issues#issue/44上有一个未解决的功能请求,请投赞成票。 - reccles
8
@Frank,我认为还没有发布针对这个问题的版本。它将会在JUnit 4.11中发布。届时(假设设计保持不变),它将提供一种文本方式来指定如何命名测试,包括采用参数作为名称。其实相当不错。 - Yishai
5
JUnit 4.11已经发布 :-) - rescdsk
7
以下是翻译的结果:为了以后参考,这里是原问题的更新链接:https://github.com/junit-team/junit/issues/44。 - kldavis4
显示剩余3条评论

19

最近我在使用JUnit 4.3.1时遇到了同样的问题。我编写了一个新类,它继承了Parameterized,并命名为LabelledParameterized。该类已经通过JUnit 4.3.1、4.4和4.5进行了测试。它通过@Parameters方法中每个参数数组第一个参数的字符串表示形式来重构Description实例。你可以在以下链接查看该代码:

http://code.google.com/p/migen/source/browse/trunk/java/src/.../LabelledParameterized.java?r=3789

还有一个使用示例:

http://code.google.com/p/migen/source/browse/trunk/java/src/.../ServerBuilderTest.java?r=3789

在Eclipse中,测试描述格式排版得很好,这正是我想要的,因为这使得查找失败的测试变得更加容易!在接下来的几天/周里,我可能会进一步完善和文档化这些类。如果你想要尝试最新版本,请删除链接中的“?”部分。:-)

要使用它,你只需要复制那个类(GPL v3),并将@RunWith(Parameterized.class)更改为@RunWith(LabelledParameterized.class),假设你的参数列表的第一个元素是一个合理的标签。

我不知道任何后续版本的JUnit是否解决了这个问题,但即使解决了,我也不能更新JUnit,因为所有的共同开发者都必须进行更新,而且我们有更高的优先级任务需要完成。因此,该类的工作是可以编译多个版本的JUnit。


注意:这里有一些反射技巧,以便它可以在不同的JUnit版本中运行(如上所列)。专门针对JUnit 4.3.1版本的版本可以在此处找到这里,而对于JUnit 4.4和4.5,则在这里找到。

今天我的一个共同开发者遇到了问题,因为我在上面的信息中使用的版本是JUnit 4.3.1(而不是最初提到的4.4)。他正在使用JUnit 4.5.0,并且引起了问题。我今天会解决这些问题。 - darrenp
我花了一些时间理解你需要在构造函数中“传递”测试名称,而不是“记住”它。感谢提供代码! - giraff
只要我从Eclipse运行测试,它就很好用。但是,有没有人在JUnit Ant任务中使用过它?生成的测试报告名称为execute [0],execute [1] ... execute [n] - Henrik Aasted Sørensen
非常好。像魔法一样运行。如果您能添加信息,说明需要将“String label,…”作为调用@Test方法的第一个参数,那就太好了。 - user166566

13

使用Parameterized模型,我编写了自己的定制测试运行程序/套件 - 只花了大约半个小时。它与darrenp的LabelledParameterized略有不同,因为它允许您显式指定名称,而不是依赖于第一个参数的toString()

它还不使用数组,因为我讨厌数组。 :)

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);
    }
  }
}

还有一个示例:

@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);
  }

}

8

1
JUnit已经迁移到了GitHub。这是更新后的链接:https://github.com/Pragmatists/JUnitParams - rrrocky

6

从junit4.8.2开始,您可以通过简单地复制Parameterized类来创建自己的MyParameterized类。在TestClassRunnerForParameters中更改getName()和testName()方法即可。


我尝试过这个,但没有帮助。在创建新类时,getParametersMethod失败了。 - java_enthu

3

对我来说,它们都没有起作用,所以我获取了Parameterized的源代码并进行修改以创建一个新的测试运行程序。我并没有做太多的更改,但现在它可以工作了!!!

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;
}
}

2
当你想要测试名称中的参数值时,可以这样做 -
@ParameterizedTest(name="{index} {arguments} then return false" )
@ValueSource(strings = {"false","FALSE","   ","123","abc"})
@DisplayName("When Feature JVM argument is ")
void test_Feature_JVM_Argument_Is_Empty_Or_Blank_Strings_Or_False(String params) {
    System.setProperty("FeatureName", params);
    assertFalse(Boolean.parseBoolean(System.getProperty("FeatureName")));
}

测试名称将显示为-

JUnit测试图像


准确地击中要害并展示预期的行为。 - Stan

2
你可以创建一个像这样的方法:
@Test
public void name() {
    Assert.assertEquals("", inboundFileName);
}

虽然我不会一直使用它,但查找测试编号143确实很有用。


2

我经常使用静态导入 Assert 等内容,这样我很容易重新定义断言:

private <T> void assertThat(final T actual, final Matcher<T> expected) {
    Assert.assertThat(editThisToDisplaySomethingForYourDatum, actual, expected);
}

例如,您可以在测试类的构造函数中初始化一个“名称”字段,并在测试失败时将其显示出来。只需将其作为每个测试的参数数组的第一个元素传递即可。这也有助于标记数据:
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}
    });
}

如果测试失败了一个断言,那么这是可以的,但还有其他情况,比如如果抛出异常导致测试失败,或者测试期望抛出异常,这些情况需要考虑框架应该处理的开销。 - Yishai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接