Lore
Lore

Reputation: 1908

Parameterizing with array in Junit 5 (or other testing Java library) in smarter fashion

I'm trying to parameterize this test:

@Test
public void reverseQuote(double[] qsp) throws Exception {
...}

It seems absurd to me that it doesn't exists some quick method to initialize array qsp like, for example, ValueSource:

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertNotNull(argument);
}

my aim is to do something like @ValueSource(doublesArray = {new double[]{1.0, 2.0, 3.0}}) (that now returns error). Doesn't exists anything that permits something similar?? Other answers seem to suggest only elaborated ways, like using @MethodSource or @ConvertWith.

I accept answers implementing other testing libraries, too.

Upvotes: 20

Views: 10920

Answers (6)

Fox user9219598
Fox user9219598

Reputation: 111

Array:

    static Stream<Arguments> yourTest () {
    return Stream.of(
            Arguments.of((new int[] { 2, 1, 2, 3, 4 }), 3),
            Arguments.of((new int[] { 2, 2, 0 }), 3),
            Arguments.of((new int[]{1, 3, 5} ) ,0 )     
            );
}
// if not Array : List : Arguments.of((Arrays.asList(0, 1) ), 0.5)...


@ParameterizedTest(name = "{index} => array   = {0} ),  expected = {1} ") 
@MethodSource("yourTest")
void shoultCountEvens(  int[] array, int expected) {
    assertEquals( CountEvens.countEvens(array),  expected ); 
}


public class CountEvens {
public static int countEvens(int[] nums) {
    long count = Arrays.stream(nums)
    .filter(a -> a %  2 == 0)
    .count();
return Math.toIntExact(count);
}}

Upvotes: 2

Shadov
Shadov

Reputation: 5592

Ok, this is gonna be a weird answer, but it works and it was kinda fun to do.

First thing: your way is impossible. Not because of JUnit or any related API, but because of Java - valid annotation type elements (annotation arguments can only be primitive, String, Class, Enum, other annotation and array of all those).

Second thing: we can get around the first one. Check this:

@ArraySources(
  arrays = {
    @ArraySource(array = {1, 2, 3}),
    @ArraySource(array = {4, 5, 6}),
    @ArraySource(array = {7, 8, 9})
  }
)

As it says, annotation can have other annotations as arguments, and arrays of those, so we are using those 2 rules here.

Third thing: how does that help? We can add our own annotation + argument provider, JUnit 5 is expansible in that way.

Both annotations:

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(ArrayArgumentsProvider.class)
public @interface ArraySources {
    ArraySource[] arrays();
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ArraySource {
    int[] array() default {};
}

Argument provider based on the annotations:

public class ArrayArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<ArraySources> {
    private List<int[]> arguments;

    public void accept(ArraySources source) {
        List<ArraySource> arrays = Arrays.asList(source.arrays());

        this.arguments = arrays.stream().map(ArraySource::array).collect(Collectors.toList());
    }

    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return this.arguments.stream().map(Arguments::of);
    }
}

And the final test using those:

public class ArraySourcesTest {
    @ParameterizedTest
    @ArraySources(
            arrays = {
                    @ArraySource(array = {1, 2, 3}),
                    @ArraySource(array = {4, 5, 6}),
                    @ArraySource(array = {7, 8, 9})
            }
    )
    void example(int[] array) {
        System.out.println(Arrays.toString(array));
        System.out.println("Test Over");
    }
}

/* Output
[1, 2, 3]
Test Over
[4, 5, 6]
Test Over
[7, 8, 9]
Test Over
*/

You mentioned @MethodSource as complicated, well, so I think I failed in this matter, but it works. It could be simplified and enhanced obviously (like naming annotation arguments as defaults - value - and I only did it for int to show the idea). Not sure if you could achieve the same with existing features (ArgumentsProvider and ArgumentSources), but this looks more specific (you know you are working with arrays) and shows possibilities of extending JUnit5, may be useful in other case.

Upvotes: 9

Dieter
Dieter

Reputation: 109

Using a combination of Junit Parameterized Tests and YAML Parsing might be something to consider.

@RunWith(Parameterized.class)
public class AnotherParameterizedTest {

    private final HashMap row;

    @Parameterized.Parameters(name="Reverse Lists Tests # {index}:")
    public static List<Map<String, Object>> data() {
        final TestData testData = new TestData(""+
             "|   ID   |       List         |  Expected   |                \n"+
             "|   0    |    [1, 2, 3]       |  [3, 2, 1]  |                \n"+
             "|   1    |    [2, 3, 5]       |  [3, 2, 1]  |                \n"+
             "|   2    |    [5, 6, 7]       |  [ 7, 6, 5] |                \n"
        );
        // parsing each row using simple YAML parser and create map per row
        return testData.getDataTable();
    }

    // Each row from the stringified table above will be 
    // split into key=value pairs where the value are parsed using a 
    // yaml parser. this way, values can be pretty much any yaml type
    // like a list of integers in this case. 
    public AnotherParameterizedTest(HashMap obj) {
        this.row = obj;
    }

    @Test
    public void test() throws Exception {
        List orgListReversed = new ArrayList((List) row.get("List"));
        Collections.reverse(orgListReversed);
        assertEquals((List) row.get("Expected"), orgListReversed);
    }

}

Instead of using a String I am using a Excel Reader to do the same with simple Excel Tables. Parsing each row into one Map using YAML for the Values.

Junit IDE Test Results

The same just tested using Junit Jupiter gives nicer results in the IDE Runner.

import static org.junit.jupiter.api.Assertions.assertEquals;

import de.deicon.yatf.runner.dsl.TestData;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Collections;
import java.util.List;
import java.util.Map;

public class FirstTest {

    @ParameterizedTest
    @MethodSource("testTable")
    public void test(Map row){
        List reversedList = (List) row.get("List");
        Collections.reverse(reversedList);
        assertEquals((List)row.get("Expected"), reversedList);
    }

    static List<Map<String, Object>> testTable() {
        return new TestData(""+
                "|ID|   List                  |Expected               |         \n"+
                "|0 | [1,2,3]                 | [3,2,1]               |         \n"+
                "|1 | [hans, peter, klaus]    | [klaus, peter, hans]  |         \n"
        ).getDataTable();
    }

}

enter image description here

Upvotes: 9

user31601
user31601

Reputation: 2610

I like using Spock for testing Java code. It's a groovy-based test framework that sits on top of JUnit 4. Parameterized tests in Spock are a built-in feature:

def "The reverseQuote method doesn't return null"(double[] qsp) {

    when: "reverseQuote is called"
    double[] rev = reverseQuote(qsp)

    then: "the result is not null"
    null != rev

    where: "there are various input values"
    qsp << [
        [0.1, 0.2, 0.3] as double[],
        [1.0, 2.0, 3.0] as double[]
    ]
}

... alternatively, you can lay out your test data in tabular form:

def "The reverseQuote method reverses the input array"(List qsp, List expected) {

    when: "reverseQuote is called"
    double[] rev = reverseQuote(qsp as double[])

    then: "the result is the reverse of the input"
    expected as double[] == rev

    where: "there are various input values"
    qsp             | expected
    [0.1, 0.2, 0.3] | [0.3, 0.2, 0.1]
    [1.0, 2.0, 3.0] | [3.0, 2.0, 1.0]
}

Note that the as double[] prevalence is an unfortunate consequence of Groovy automatically converting arrays to Lists, so we have to explicitly cast them back in the particular cases where we are interacting with Java code that actually requires an array.

Upvotes: 5

Ben Sch
Ben Sch

Reputation: 104

The JUnit documentation suggests to use @MethodSource

@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(double argument) {
    assertNotEquals(9.0, argument);
}

static DoubleStream range() {
   return DoubleStream.range(0.0, 20.0);
}

Otherwise you could consider using this: https://junit.org/junit5/docs/5.0.2/api/org/junit/jupiter/params/provider/ValueSource.html#doubles

@ParameterizedTest
@ValueSource(doubles = { 1, 2, 3 })
public void test(double numberToTest){
   //do whatever
}

Upvotes: -1

KrzyH
KrzyH

Reputation: 4316

You can check TestNg (I'm using it in my project). Example in my project you can check here enter link description here or below:

  @DataProvider(name = "processText")
  public Object[][] dataProvider() {
    return new Object[][]{
      new Object[]{"ala\nma\nkota", "grep ma", "ma"},
      new Object[]{"ala\nma\nkota", "grep -v ma", "ala\nkota"},
      new Object[]{"ala\nma\nkota", "cut -c1-3", "ala\nma\nkot"},
      //...
      new Object[]{"ala ma kota", "sed s/ma/XX/g", "ala XX kota"},
      new Object[]{"ala\nma\nkota", "grep -v ma | sed s/a/G/g", "GlG\nkotG"},
    };
  }

  @Test(dataProvider = "processText")
  public void testProcessText(String text, String cli, String expected) {
    final String actual = new UnixProcessing().processText(text, cli);
    assertEquals(actual, expected);
  }

Official TestNg documentation is here

Upvotes: -2

Related Questions