Search code examples
javatestingjunit

Multiple value sources for @ParameterizedTests


Here's what I want

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class HsbColorTest {
    @ParameterizedTest
    @ValueSource(floats = {0, 100, 200, 360}) // mapped to hue (0-360)
    @ValueSource(floats = {0, 35, 100}) // mapped to saturation (0-100)
    @ValueSource(floats = {0, 35, 100}) // mapped to brightness (0-100)
    void create_withValidArgs_createsExpectedColor(float hue, float saturation, float brightness) {
        HsbColor hsbColor = new HsbColor(hue, saturation, brightness);
        assertEquals(hue, hsbColor.getHue());
        assertEquals(saturation, hsbColor.getSaturation());
        assertEquals(brightness, hsbColor.getBrightness());
    }
}

Unfortunately, it's not compilable since @ValueSource is not @Repeatable

How can I achieve a similar effect in JUnit 5?

I can do this:

    @ParameterizedTest
    @CsvSource({
            "0, 0, 0",
            "0, 35, 35",
            "0, 100, 100",
            "100, 0, 0",
            "100, 35, 35",
            "100, 100, 100",
            "200, 0, 0",
            "200, 35, 35",
            "200, 100, 100",
            "360, 0, 0",
            "360, 35, 35",
            "360, 100, 100"
    })
    void create_withValidArgs_createsExpectedColor(float hue, float saturation, float brightness) {
        HsbColor hsbColor = new HsbColor(hue, saturation, brightness);
        assertEquals(hue, hsbColor.getHue());
        assertEquals(saturation, hsbColor.getSaturation());
        assertEquals(brightness, hsbColor.getBrightness());
    }

The drawback: it's clumsy and not very readable

I can write three separate tests, but the problem is I can't test validation of three different param values in isolation. For example, a test that ensures that valid hue values are accepted would in effect test that the other two values passed validation as well (otherwise, the "hue test" would fail)

For the purposes of this question, assume HsbColor is a simple immutable data class with hue, saturation, brightness properties (and getters)

public class HsbColor {
    private final float hue;
    private final float saturation;
    private final float brightness;

    public HsbColor(float hue, float saturation, float brightness) {
        if (hue < 0 || hue > 360) {
            throw new IllegalArgumentException("Hue must be between 0 and 360");
        }
        if (saturation < 0 || saturation > 100) {
            throw new IllegalArgumentException("Saturation must be between 0 and 100");
        }
        if (brightness < 0 || brightness > 100) {
            throw new IllegalArgumentException("Brightness must be between 0 and 100");
        }
        this.hue = hue;
        this.saturation = saturation;
        this.brightness = brightness;
    }

    public float getHue() {
        return hue;
    }

    public float getSaturation() {
        return saturation;
    }

    public float getBrightness() {
        return brightness;
    }
}

Solution

  • You are looking for the JUnit Pioneer Extension to JUnit Jupiter, especially it's cartesian test: https://junit-pioneer.org/docs/cartesian-product/

    import org.junitpioneer.jupiter.cartesian.ArgumentSets;
    import org.junitpioneer.jupiter.cartesian.CartesianTest;
    
    class HsbColorTest {
    
        record SaturationAndBrightness(float s, float b) {
        }
    
        static ArgumentSets create_withValidArgs_createsExpectedColor() {
            return ArgumentSets.argumentsForFirstParameter(0f, 100f, 200f, 360f).argumentsForNextParameter(
                    new SaturationAndBrightness(0, 0), new SaturationAndBrightness(35, 35),
                    new SaturationAndBrightness(100, 100));
        }
    
        @CartesianTest
        @CartesianTest.MethodFactory("stringClassTimeUnitFactory")
        void create_withValidArgs_createsExpectedColor(float hue, SaturationAndBrightness saturationAndBrightness) {
            var saturation = saturationAndBrightness.s;
            var brightness = saturationAndBrightness.b;
            
            HsbColor hsbColor = new HsbColor(hue, saturation, brightness);
            assertEquals(hue, hsbColor.getHue()); assertEquals(saturation,
            hsbColor.getSaturation()); assertEquals(brightness, hsbColor.getBrightness());
             
            System.out.println(hue + "; " + saturation + ", " + brightness);
        }
    
    }
    

    if you don't wanna bring in an extension, this works equally well, as your second code example does not really show a cartesian product:

    import java.util.List;
    import java.util.stream.Stream;
    
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.Arguments;
    import org.junit.jupiter.params.provider.MethodSource;
    
    class HsbColorTest {
    
        record SaturationAndBrightness(float s, float b) {
        }
    
        static Stream<Arguments> create_withValidArgs_createsExpectedColor() {
    
            var values = List.of(
                new SaturationAndBrightness(0, 0),
                new SaturationAndBrightness(35, 35),
                new SaturationAndBrightness(100, 100)
            );
    
            return Stream.of(0f, 100f, 200f, 360f)
                .flatMap(hue ->
                     values.stream().map(v -> Arguments.of(hue, v.s, v.b))
                );
        }
    
        @ParameterizedTest
        @MethodSource
        void create_withValidArgs_createsExpectedColor(float hue, float saturation, float brightness) {
            HsbColor hsbColor = new HsbColor(hue, saturation, brightness);
            assertEquals(hue, hsbColor.getHue()); assertEquals(saturation,
            hsbColor.getSaturation()); assertEquals(brightness, hsbColor.getBrightness());
    
            System.out.println(hue + "; " + saturation + ", " + brightness);
        }
    
    }
    

    With just textual annotations, I don't think it's easily possible other than you enumerated manually.