Search code examples
javajunit5parameterized-unit-test

Parameterize both class and tests in JUnit 5


Is there a way to parameterize both test class (like you could do with Parameterized and @Parameters in JUnit 4) and test methods (like you could do with JUnitParams in JUnit 4 or with @ParameterizedTest in JUnit 5)? I need to get the Cartesian product of the parameters in the end.

Example of a partial test for java.nio.ByteBuffer using the desired approach:

public class ByteBufferTest {
    private static final int BUFFER_SIZE = 16384;
    private final ByteOrder byteOrder;
    private ByteBuffer sut;

    @Factory(dataProvider = "byteOrders")
    public ByteBufferTest(ByteOrder byteOrder) {
        this.byteOrder = byteOrder;
    }

    @DataProvider
    public static Object[][] byteOrders() {
        return new Object[][] {
                {ByteOrder.BIG_ENDIAN},
                {ByteOrder.LITTLE_ENDIAN}
        };
    }

    @BeforeMethod
    public void setUp() {
        sut = ByteBuffer.allocate(BUFFER_SIZE);
        sut.order(byteOrder);
    }

    @Test(dataProvider = "validPositions")
    public void position(int position) {
        System.out.println(byteOrder + " position " + position);
        sut.position(position);
        assertThat(sut.position()).isEqualTo(position);
    }

    @DataProvider
    public static Object[][] validPositions() {
        return new Object[][] {{0}, {1}, {BUFFER_SIZE - 1}};
    }

    @Test(dataProvider = "intPositionsAndValues")
    public void putInt(int position, int value, byte[] expected) {
        System.out.println(byteOrder + " position " + position + " value " + value);
        sut.putInt(position, value);
        assertThat(sut.array())
                .contains(expected[0], atIndex(position))
                .contains(expected[1], atIndex(position + 1))
                .contains(expected[2], atIndex(position + 2))
                .contains(expected[3], atIndex(position + 3));
    }

    @DataProvider
    public Object[][] intPositionsAndValues() {
        if (byteOrder == ByteOrder.BIG_ENDIAN) {
            return new Object[][]{
                    {0, 0, new byte[4]},
                    {5, 123456789, new byte[] {0x07, 0x5B, (byte) 0xCD, 0x15}},
            };
        } else {
            return new Object[][]{
                    {0, 0, new byte[4]},
                    {5, 123456789, new byte[] {0x15, (byte) 0xCD, 0x5B, 0x07}},
            };
        }
    }
}

It produces:

LITTLE_ENDIAN position 0
LITTLE_ENDIAN position 1
LITTLE_ENDIAN position 16383
BIG_ENDIAN position 0
BIG_ENDIAN position 1
BIG_ENDIAN position 16383
LITTLE_ENDIAN position 0 value 0
LITTLE_ENDIAN position 5 value 123456789
BIG_ENDIAN position 0 value 0
BIG_ENDIAN position 5 value 123456789

We're thinking about migrating to JUnit 5 from TestNG, but we use this kind of thing pretty often. The use of the byte order as a class-level parameter in the example above is not a coincidence: we often need tests for various binary data processor, where the test constructor would take a byte/bit order argument, and we run every test for both Big Endian and Little Endian.

I was thinking about creating an extension for this and then use ExtendWith, but maybe there is an existing extension or something that works out-of-the-box that I have missed?


Solution

  • JUnit Jupiter (Vanilla)

    You can combine multiple sources within e.g. a @MethodSource. Based on your TestNG example:

    class ExampleTest {
    
        @ParameterizedTest
        @MethodSource("args")
        void test(String classParameter, String testParameter) {
            System.out.println(classParameter + " " + testParameter);
        }
    
        static Stream<Arguments> args() {
            return classParameters().flatMap(
                    classParameter -> testParameters().map(
                            testParameter -> Arguments.of(classParameter, testParameter)));
        }
    
        static Stream<String> classParameters() {
            return Stream.of("classParam1", "classParam2");
        }
    
        static Stream<String> testParameters() {
            return Stream.of("testParam1", "testParam2");
        }
    
    }
    

    This produces:

    classParam1 testParam1
    classParam1 testParam2
    classParam2 testParam1
    classParam2 testParam2
    

    As requested by the OP, here is "an example with at least two test methods with different set of parameters":

    class ExampleTest {
    
        static Stream<String> classParams() {
            return Stream.of("classParam1", "classParam2", "classParam3");
        }
    
        static Stream<Arguments> withClassParams(List<?> methodParams) {
            return classParams().flatMap(
                    classParam -> methodParams.stream().map(
                            methodParam -> Arguments.of(classParam, methodParam)));
        }
    
        @ParameterizedTest
        @MethodSource
        void booleanParams(String classParam, boolean booleanParam) {
            System.out.println(classParam + " " + booleanParam);
        }
    
        static Stream<Arguments> booleanParams() {
            return withClassParams(List.of(false, true));
        }
    
        @ParameterizedTest
        @MethodSource
        void integerParams(String classParam, int integerParam) {
            System.out.println(classParam + " " + integerParam);
        }
    
        static Stream<Arguments> integerParams() {
            return withClassParams(List.of(1, 2, 3, 4, 5, 6));
        }
    
        @ParameterizedTest
        @MethodSource
        void objectParams(String classParam, Object objectParam) {
            System.out.println(classParam + " " + objectParam);
        }
    
        static Stream<Arguments> objectParams() {
            return withClassParams(List.of(new Object()));
        }
    
    }
    

    3 class parameters plus 3 different method parameters with different types and sizes, producing the following output:

    classParam1 java.lang.Object@35cabb2a
    classParam2 java.lang.Object@35cabb2a
    classParam3 java.lang.Object@35cabb2a
    classParam1 1
    classParam1 2
    classParam1 3
    classParam1 4
    classParam1 5
    classParam1 6
    classParam2 1
    classParam2 2
    classParam2 3
    classParam2 4
    classParam2 5
    classParam2 6
    classParam3 1
    classParam3 2
    classParam3 3
    classParam3 4
    classParam3 5
    classParam3 6
    classParam1 false
    classParam1 true
    classParam2 false
    classParam2 true
    classParam3 false
    classParam3 true
    

    JUnit Pioneer

    There is the JUnit Pioneer extension pack for JUnit Jupiter. It comes with @CartesianTest. Using the extended the example from above:

    class CartProdTest {
        
        @CartesianTest
        @CartesianTest.MethodFactory("classWithObjectParams")
        void testClassWithObject(String clazz, Object object) {
            System.out.println(clazz + " " + object);
        }
    
        static ArgumentSets classWithObjectParams() {
            return ArgumentSets
                    .argumentsForFirstParameter(classParams())
                    .argumentsForNextParameter(new Object());
        }
    
        @CartesianTest
        @CartesianTest.MethodFactory("classWithIntegerParams")
        void testClassWithInteger(String clazz, int integerParam) {
            System.out.println(clazz + " " + integerParam);
        }
    
        static ArgumentSets classWithIntegerParams() {
            return ArgumentSets
                    .argumentsForFirstParameter(classParams())
                    .argumentsForNextParameter(1, 2, 3, 4, 5, 6);
        }
    
        @CartesianTest
        @CartesianTest.MethodFactory("classWithBooleanParams")
        void testClassWithBoolean(String clazz, boolean booleanParam) {
            System.out.println(clazz + " " + booleanParam);
        }
    
        static ArgumentSets classWithBooleanParams() {
            return ArgumentSets
                    .argumentsForFirstParameter(classParams())
                    .argumentsForNextParameter(false, true);
        }
    
        static String[] classParams() {
            return new String[]{"classParam1", "classParam2", "classParam3"};
        }
    
    }
    

    This produces the same output.