Genhis
Genhis

Reputation: 1524

How to test methods which depend on initial parameters?

In my test class, I have several parameterized methods where parameters are a part of object initialization. I use the same parameters for all of them, and then I test methods which depend on one or more of the parameters.

@MethodSource(value = "validSizes")
@ParameterizedTest
void testGetHeight(int terrainWidth, int height) {
    World world = new World(terrainWidth, height);
    assertEquals(height, world.getHeight());
}

This may lead to code duplication - I need to repeat parameters, World construction and @MethodSource annotation.

I would like to avoid code duplication because if I add another parameter to the constructor, I would have to change all of the testing methods which depend on the parameters.

Can I do it with @BeforeEach annotation, i.e. parametrize the setUp() method, or do it another way? Is it a good practice to test such methods with different parameters?

Upvotes: 3

Views: 1513

Answers (2)

Skym0sh0
Skym0sh0

Reputation: 315

You could write your own Argument Provider which provides a fully constructed world object and injects it into your setup/BeforeEach method (where you store it for usage in your test methods). That way you minimize your setup code and condensate your initialization logic to one specific place in your code.

Edit: I had a wrong mechanism in mind. SO here is the right example and its clarification what I meant:

Lets assume your world object looks something like this:

class World {
    private final int width;
    private final int height;

    public World(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public String toString() {
        return "World{" + "width=" + width + ", height=" + height + '}';
    }
}

And you have several Test methods where you need a set of different World objects (possibly dynamically created), something like this:

class ArgumentProviderTest {
    @ParameterizedTest
    @ArgumentsSource(WorldArgumentProvider.class)
    void yourFirstTest(World w) {
        System.out.println(w);
    }

    @ParameterizedTest
    @ArgumentsSource(WorldArgumentProvider.class)
    void yourSecondTest(World w) {
        System.out.println(w);
    }
}

Here you see, the tests all expect a World object as Argument. Now we need a way to provide world objects. For this we write our own ArgumentsProvider which is also straight-forward:

class WorldArgumentProvider implements ArgumentsProvider {

    private Collection<World> createWorlds() {
        // Somehow create a bunch of World objects
        // !!! Here goes your initialization logic. !!!
        return ThreadLocalRandom.current()
                .ints()
                .limit(10)
                .mapToObj(i -> new World(i % 100, i / 100))
                .collect(Collectors.toList());
    }

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        // create an Arguments object from these (discrete) worlds.
        // remember: one method takes one Arguments object which carries each single argument/parameter
        return createWorlds().stream().map(Arguments::of);
    }
}

And here you can do whatever you want with full Compiler support (instead of using method name as strings).

Maybe you could achieve a similar behaviour using DynamicTests with a @TestFactory, but that depends on your exact tests.


My first thought was to use a ParameterResolver which basically works like this:

  1. Register ParameterResolver as Extension (in this context on class level instead of method level)
  2. This resolver is called for every (unresolved) Parameter of your Test- and Lifecycle methods (BeforeEach/AfterAll/...)
  3. It decides if it actually can resolve this parameter (e.g. right type, right index, whatever)
  4. It provides the actual Argument as parameter

However, with this mechanism you can not tell the Jupiter engine to construct multiple Test instances for a method (Jupiter introduced the concept of TestTemplate for such a thing). Interestingly, this Parameterized-Class conect was implemented in jUnit-4 (as the only way of parameterized tests) and is AFAIK a feature hopefully coming in the future in Jupiter. I am looking forward to it.

Upvotes: 2

Genhis
Genhis

Reputation: 1524

You could use a wrapper class to have all initialization code in one place.

class WorldWrapper {
    public final World world;
    public final int terrainWidth;
    public final int height;

    //constructor to initialize the fields
}

You still would need to annotate each method with @MethodSource and repeat a single parameter but wouldn't have to worry if you decide to change constructor parameters.

@MethodSource(value = "validSizes")
@ParameterizedTest
void testGetHeight(WorldWrapper param) {
    assertEquals(param.height, param.world.getHeight());
}

Upvotes: 1

Related Questions