225

Is there a way to set my own custom test case names when using parameterized tests in JUnit4?

I'd like to change the default — [Test class].runTest[n] — to something meaningful.

Pops
  • 30,199
  • 37
  • 136
  • 151
Epaga
  • 38,231
  • 58
  • 157
  • 245

15 Answers15

336

This feature has made it into JUnit 4.11.

To use change the name of parameterized tests, you say:

@Parameters(name="namestring")

namestring is a string, which can have the following special placeholders:

  • {index} - the index of this set of arguments. The default namestring is {index}.
  • {0} - the first parameter value from this invocation of the test.
  • {1} - the second parameter value
  • and so on

The final name of the test will be the name of the test method, followed by the namestring in brackets, as shown below.

For example (adapted from the unit test for the Parameterized annotation):

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

will give names like testFib[1: fib(1)=1] and testFib[4: fib(4)=3]. (The testFib part of the name is the method name of the @Test).

rescdsk
  • 8,739
  • 4
  • 36
  • 32
  • 4
    There is no reason it would not be in 4.11, it's in master. Now when 4.11 will be available, that is a good question :-) – Matthew Farwell Jun 27 '12 at 09:32
  • 1
    4.11 is now in beta, and can be downloaded from the same link as above :-) – rescdsk Oct 17 '12 at 18:59
  • 2
    Yes, but there is a bug. If you put a parenthesis in the parameter "name" value like you are doing in this posting, it breaks the display of the unit test name in Eclipse. – djangofan Nov 17 '12 at 03:17
  • @djangofan, are you saying that's an Eclipse bug? I'm not working with Eclipse on a daily basis anymore, so I can't confirm. Or is it a jUnit bug? – rescdsk Dec 21 '12 at 19:46
  • @rescdsk - im not sure. the parameter name that is displayed in the JUNit tab in eclipse gets munged (when parenthesis exists in the string). – djangofan Dec 21 '12 at 21:39
  • 11
    great, but what if `{0}` and `{1}` are arrays? JUnit should ideally call `Arrays.toString({0})`, not `{0}.toString()`. For example, my `data()` method returns `Arrays.asList(new Object[][] {{ new int[] { 1, 3, 2 }, new int[] { 1, 2, 3 } }});`. – dogbane Mar 26 '13 at 11:12
  • 1
    @djangofan This is an 8 year old Eclipse bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=102512 – Pool Aug 29 '13 at 08:06
  • @Pool - that is not the same bug. the bug I speak of is for JUnit version 4.11 only. – djangofan Aug 29 '13 at 22:00
  • @djangofan - I believe it will only show up with JUnit version 4.11 due to the possibility to add brackets. You wrote `the parameter name that is displayed in the JUNit tab in eclipse gets munged (when parenthesis exists in the string)`. The bug report details that Eclipse removes detail in brackets and that it is done deliberately. Isn't this what you meant by munged? – Pool Aug 30 '13 at 08:27
  • @Pool - yes, its a related bug but I doubt its the same. – djangofan Aug 30 '13 at 18:59
  • @djangofan - OK, so what happens for the name for you? The characters are corrupted and display garbled? That sounds a more serious issue and it would be worth reporting the bug if it is still occurring. – Pool Aug 31 '13 at 16:52
  • I use JUnit 4.9 and Parameters has no name(). – Valentyn Zakharenko Oct 05 '16 at 21:59
  • 1
    There is no way I can do anything with it, right? Like `name = makeNameForObj(xy)`? – Bowi Feb 02 '17 at 13:36
  • 1
    @Bowi you can make class NewClass extends YourTestObject and override its toString method to anything you like – aelimill Feb 04 '17 at 15:46
  • for JUnit 5 it is `@ParameterizedTest(name = "{0}")` – Florian Koch Feb 01 '23 at 11:22
38

Looking at JUnit 4.5, its runner clearly doesn't support that, as that logic is buried inside a private class inside the Parameterized class. You could not use the JUnit Parameterized runner, and create your own instead which would understand the concept of names (which leads to the question of how you might set a name ...).

From a JUnit perspective, it would be nice if instead of (or in addition to) just passing an increment, they would pass the comma delimited arguments. TestNG does this. If the feature is important to you, you can comment on the yahoo mailing list referenced at www.junit.org.

Roy Tinker
  • 10,044
  • 4
  • 41
  • 58
Yishai
  • 90,445
  • 31
  • 189
  • 263
  • 4
    I would highly appreciate if there's a improvement for this in JUnit! – guerda Mar 16 '09 at 16:09
  • 1
    Instead of passing an argument perhaps just using the toString() on the test class. I imagine adding this feature to JUnit would be pretty straightforward. – reccles Feb 05 '10 at 14:42
  • 18
    Just checked, there is an outstanding feature request for this at: http://github.com/KentBeck/junit/issues#issue/44 Please vote it up. – reccles Feb 05 '10 at 14:47
  • Looks like it made it in, and will be in the next release of JUnit :-) https://github.com/KentBeck/junit/commit/3a5c9f2731462e36dd1c173ea8840d7b9b34b0ab – rescdsk Apr 13 '12 at 15:32
  • Whats the current answer regarding this issue? – Franz Ebner May 24 '12 at 09:39
  • 8
    @Frank, I think that the release that addresses this issue is not yet released. It will be in JUnit 4.11. At that time (assuming the design remains the same) it will be about a textual way of specifying how you name the test, including taking parameters as names. Pretty nice, actually. – Yishai May 24 '12 at 15:20
  • 5
    JUnit 4.11 has now been released :-) – rescdsk Nov 16 '12 at 21:38
  • 7
    Here is the updated link to the original issue https://github.com/junit-team/junit/issues/44 for future reference – kldavis4 Mar 26 '13 at 13:53
19

I recently came across the same problem when using JUnit 4.3.1. I implemented a new class which extends Parameterized called LabelledParameterized. It has been tested using JUnit 4.3.1, 4.4 and 4.5. It reconstructs the Description instance using the String representation of the first argument of each parameter array from the @Parameters method. You can see the code for this at:

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

and an example of its use at:

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

The test description formats nicely in Eclipse which is what I wanted since this makes failed tests a lot easier to find! I will probably further refine and document the classes over the next few days/weeks. Drop the '?' part of the URLs if you want the bleeding edge. :-)

To use it, all you have to do is copy that class (GPL v3), and change @RunWith(Parameterized.class) to @RunWith(LabelledParameterized.class) assuming the first element of your parameter list is a sensible label.

I don't know if any later releases of JUnit address this issue but even if they did, I can't update JUnit since all my co-developers would have to update too and we have higher priorities than re-tooling. Hence the work in the class to be compilable by multiple versions of JUnit.


Note: there is some reflection jiggery-pokery so that it runs across the different JUnit versions as listed above. The version specifically for JUnit 4.3.1 can be found here and, for JUnit 4.4 and 4.5, here.

darrenp
  • 4,265
  • 2
  • 26
  • 22
  • :-) One of my co-developers today had a problem with it since the version I point to in the above message uses JUnit 4.3.1 (not 4.4 as I originally said). He is using JUnit 4.5.0 and it caused problems. I'll be addressing these today. – darrenp Jan 13 '10 at 09:51
  • I took some time to understand that you need to _pass_ the test name in the constructor, but not _memorize_ it. Thanks for the code! – giraff Aug 06 '11 at 14:13
  • Works great as long as I run the tests from Eclipse. Does anyone have experience with making it work with the JUnit Ant Task, though? The test reports are named `execute[0], execute[1] ... execute[n]` in the test reports generated. – Henrik Aasted Sørensen Oct 10 '12 at 13:18
  • Very nice. Works like a charm. Would be nice, if you could add the info, that it is required to add "String label, ..." as first parameter to the invoked @Test-method. –  Jul 11 '13 at 06:49
13

With Parameterized as a model, I wrote my own custom test runner / suite -- only took about half an hour. It's slightly different from darrenp's LabelledParameterized in that it lets you specify a name explicitly rather than relying on the first parameter's toString().

It also doesn't use arrays because I hate arrays. :)

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

And an example:

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

}
David Moles
  • 48,006
  • 27
  • 136
  • 235
8

You may also want to try JUnitParams: https://github.com/Pragmatists/JUnitParams

Stephan
  • 41,764
  • 65
  • 238
  • 329
dsaff
  • 498
  • 5
  • 4
  • 1
    JUnit has moved to github. Here is the updated link: https://github.com/Pragmatists/JUnitParams – rrrocky Mar 30 '17 at 02:37
6

from junit4.8.2, you can create your own MyParameterized class by simply copy Parameterized class. change the getName() and testName() methods in TestClassRunnerForParameters.

yliang
  • 61
  • 1
  • 1
3

None of it was working for me, so I got the source for Parameterized and modified it create a a new test runner. I didn't have to change much but IT WORKS!!!

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;
}
}
Christian
  • 31
  • 1
2

You can create a method like

@Test
public void name() {
    Assert.assertEquals("", inboundFileName);
}

While I wouldn't use it all the time it would be useful to figure out exactly which test number 143 is.

2

A workaround would be to catch and nest all Throwables into a new Throwable with a custom message that contains all information about the parameters. The message would appear in the stack trace. This works whenever a test fails for all assertions, errors and exceptions as they are all subclasses of Throwable.

My code looks like this:

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

}

The stack trace of the failed test is:

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
mmirwaldt
  • 843
  • 7
  • 17
2

I make extensive use of static import for Assert and friends, so it is easy for me to redefine assertion:

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

For example, you could add a "name" field to your test class, initialized in the constructor, and display that on test failure. Just pass it in as the first elements of your parameters array for each test. This also helps label the data:

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}
    });
}
binkley
  • 573
  • 1
  • 6
  • 8
  • This is fine if the test fails an assert, but there are other cases, such as if an exception is thrown that fails the test, or if the test is expecting an exception to be thrown, that make thinking of the name overhead that should be handled by the framework. – Yishai Jan 14 '10 at 20:44
2

When you want the parameter values in test name then you can do something like -

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

Test name will look like -

JUnit Test image

0

Since the parameter accessed (e.g. with "{0}" always returns the toString() representation, one workaround would be to make an anonymous implementation and override toString() in each case. For example:

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...
    );
}
Sina Madani
  • 1,246
  • 3
  • 15
  • 27
0

Parameterized test is calling toString() internally. If you create an object wrapper overiding toString(), it will change the names of the test.

Here is an example, I answered in other post. https://stackoverflow.com/a/67023556/1839360

Yan Khonski
  • 12,225
  • 15
  • 76
  • 114
0

For a more complex object you may do the following (example with JUnit 4):

@RunWith(Parameterized.class)
public class MainTest {
    private static Object[] makeSample(String[] array, int expectedLength) {
        return new Object[]{array, expectedLength, Arrays.toString(array)};
    }

    @Parameterized.Parameters(name = "for input {2} length should equal {1}")
    public static Collection<Object[]> data() {
        return Arrays.asList(
                makeSample(new String[]{"a"}, 1),
                makeSample(new String[]{"a", "b"}, 2)
        );
    }
    private final int expectedLength;
    private final String[] array;

    public MainTest(String[] array, int expectedLength, String strArray) {
        this.array = array;
        this.expectedLength = expectedLength;
    }

    @Test
    public void should_have_expected_length() {
        assertEquals(expectedLength, array.length);
    }
}

The trick here is to use one input parameter as a string describing either some part of input or the whole test case.

Before adding third parameter it looked like this enter image description here

And after like this

enter image description here

xuesheng
  • 3,396
  • 2
  • 29
  • 38
0

Check out JUnitParams as dsaff mentioned, works using ant to build parameterized test method descriptions in the html report.

This was after trying LabelledParameterized and finding that it although it works with eclipse it does not work with ant as far as the html report is concerned.

Cheers,

quarkonium
  • 128
  • 6