logo
Solutionunittest

JUnit 5 parameterized tests

Views: 8007 Created: 2019-09-17 Read time: 6 minutes
Post Preview
Tags:

1)     Preface

I am quite sure the Junit 4 experience with parameterisation did leave a bad taste in your mouth.

Have no fear, with the Junit 5 coming into the picture we have this aspect covered to the point when it is a shame you do not need to use it for some tests.

In this Juint 5 episode, we will take a look at this, oh so crucial feature, and will try to conquer it!

 

Note
Note

 

2)     Junit 5 @ValueSource: single input parameterisation

 

If your test needs only a single argument to be provided for its execution, then @ValueSource can quickly satisfy that requirement. Any primitive type along with java.lang.String and java.lang.Class are allowed to be passed.

Let's go through an example where our Hero will be trying to steal a certain amount of gold from the Dragon:

SUT Code


public boolean stealGoldFromDragon(Hero hero, Dragon dragon, int amountToSteal)
		throws HeroSlainedByDragonException {
	if(amountToSteal > 2000){
		throw new RuntimeException("You cannot try to steal more than 2000 gold at a time.");
	}

	if(amountToSteal >= Integer.valueOf(1000)){
		boolean dragonSlained = gameEngine.fightWithDragon(hero, dragon);

		if(!dragonSlained){
			throw new HeroSlainedByDragonException();
		}
	}

	return gameEngine.stealGold(hero, dragon);
}

Here we are:

        (L4):  Defining a limit for the gold to be stolen. Let us just not be that greedy.

        (L8):  If Hero tries to steal at least 1000 gold, then Dragon will not let that happen that easily. There is a fight going on in that case, and either Hero wins or...

        (L16): Hero tries to steal the gold

 

We will try to write a test for a scenario where the hero is unfortunately slained. This might happen only if he tries to steal between 1000 and 2000 gold from the Dragon:

 

Note
Note

 

Test Code


import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;

@InjectMocks private GameController gameControllerSUT;
@Mock private GameEngine gameEngineMock;

@ParameterizedTest
@ValueSource(ints = {1001,1500,2000})
public void shouldThrowException_whenHeroSlained_givenTryingToStealGold(
		int goldAmountToSteal
) throws Exception{
	// Given
	Hero hero = new Hero();
	Dragon dragon = new Dragon();

	given(gameEngineMock.fightWithDragon(hero, dragon))
			.willThrow(new HeroSlainedByDragonException());

	// When
	assertThatThrownBy(() -> gameControllerSUT.stealGoldFromDragon(hero, dragon, goldAmountToSteal))
	// Then
			.isInstanceOf(HeroSlainedByDragonException.class);

}

Here we are:

        (L13):  Marking the test as parameterised. This is necessary for the @ValueSource to work.

        (L14):  We define the array of inputs that would be fed into the SUT method.

        (L22):  We make sure that when actually a fight occurs, the Hero will not win.

        (L26-28):  SUT is invoked, and exception verified to be of the expected type.

 

If you are not familiar with Mockito and BDD, this article might help you add these into your automated testing repertoire.

 

Reference
Reference
unittest
BDD Mockito

 

Note
Note

 

 

3)     Junit 5 @NullSource and @EmptySource: lack of data is also a case-study!

Sometimes we would like to test the border-line cases. More precisely, when the data is not supported at all. We cannot pass NULL values directly in the @ValueSource attribute arrays. Junit 5 would like to you to use @NullSource and @EmptySource in these scenarios. In my opinion, this is a great way to clearly document and underline what are we doing here.

This time our Hero will be trying to steal specific Treasures from the Dragon:

 

SUT Code


public boolean stealTreasureFromDragon(Hero hero, Dragon dragon, List<Treasure> treasures)
		throws HeroIsAChickenExcpetion {
	if(CollectionUtils.isEmpty(treasures)){
		throw new HeroIsAChickenExcpetion();
	}

	return gameEngine.stealTreasures(hero, dragon, treasures);
}

 

Now we would like to test scenarios where Hero decides not to steal anything at all:

 

Test Code


import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

@ParameterizedTest
@NullSource
@EmptySource
public void shouldThrowException_whenNoTreasureMeantToBeStolen(
	List<Treasure> treasures) throws Exception{
	// Given
	Hero hero = new Hero();
	Dragon dragon = new Dragon();

	// When
	assertThatThrownBy(() -> gameControllerSUT
								.stealTreasureFromDragon(hero,dragon,treasures))
	// Then
			.isInstanceOf(HeroIsAChickenExcpetion.class);
}

In this test method, we are:

        (L11):  Defining that we would like to pass the NULL value as one of the params for this test.

        (L12):  Defining that we would like to pass an empty value as one of the params for this test. The nature of the empty value that is that it is based on the type of the parameter itself. For example, if String were the type of the input param then an empty string would be passed instead of an empty Collection.

 

Tip
Tip

 

4)     Junit 5 @EnumSource: enumerated input parameterisation

A lot of times the engine of our SUT would be reliant on a specific Enum value. JUnit 5 brings into the table explicit support for cases like these in the form of @EnumeratedValue parameterised test.

This time our Hero will be fighting with a MightyDragon:

 

SUT Code


public boolean fightWithMightyDragon(Hero hero, MightyDragon mightyDragon)
	throws DoesNotStandAChanceException {
	BuffType buffType = hero.getActiveBuff().getBuffType();
	boolean dragonSlained = false;

	if(buffType.equals(BuffType.INCREASED_AGILITY)
		|| buffType.equals(BuffType.INCREASED_STRENTH)
			|| buffType.equals(BuffType.INCREASED_SPEED)){
		dragonSlained = gameEngine.fightWithDragon(hero, mightyDragon);
	}else{
		throw new DoesNotStandAChanceException();
	}

	return dragonSlained;
}

In this SUT method, we are:

        (L6-8):  Checking whether our Hero has one of the required buffs that are the bare minimum to even think about fighting the MightyDragon.

        (L9): GameEngine simulates the fight.

        (L11):  Throwing an adequate exception when Hero does not have one of the required buffs.

 

Now we will write a test where our Hero actually slains the dragon. That might happen if we set certain types of buffs for the Hero. As BuffType is an enum, we will take advantage of the @EnumSource annotation:

 

Test Code


import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;

@InjectMocks private GameController gameControllerSUT;
@Mock private GameEngine gameEngineMock;

@ParameterizedTest
@EnumSource(value = BuffType.class, mode = EnumSource.Mode.INCLUDE,
		names = {"INCREASED_STRENTH" ,"INCREASED_AGILITY" ,"INCREASED_SPEED"})
public void shouldWinWithMightyDragon(BuffType buffType) throws Exception{
	// Given
	Hero hero = new Hero();
	MightyDragon dragon = new MightyDragon();
	hero.setActiveBuff(new Buff("Active buff", buffType));

	given(gameEngineMock.fightWithDragon(hero, dragon))
			.willReturn(true);

	// When
	boolean dragonSlained = gameControllerSUT.fightWithMightyDragon(hero, dragon);

	// Then
	assertThat(dragonSlained).isTrue();
}

In this test method, we are:

        (L13-14):  Defining our test as parameterised. Remember to use @ParameterizedTest instead of vanilla @Test.

        (L20):  Setting an active buff for the Hero.

        (L22):  Making sure that Hero wins the fight once it occurs.

        (L26):  Invoking the SUT method.

 

Tip
Tip

 

Note
Note

 

5) Junit 5 @MethodSource: parameterising multiple inputs

In most cases, the single argument @ValueSource would be enough to satisfy the parameterisation needs. Provided, that the SUT method is simple enough and conforms really well with the SRP principle. Sometimes specific scenarios would happen but even when more than one variable is present in the equation. This is where the @MethodSource comes into play.

To illustrate this feature, we will take a look behind our hero's shoulder and see how he counts his hardly earned loot:

 

SUT Code


public BigDecimal countLoot(List<Treasure> treasures, Hero hero){
	BigDecimal lootWorth = hero.getGold();

	return treasures.stream()
			.map(treasure -> treasure.getTreasureType().getWorth())
			.reduce(lootWorth,
					(totalWorth, treasureWorth) -> totalWorth.add(treasureWorth));
}

In this simple SUT method, we are calculating the worth of each of the Treasure pieces our hero has gathered and adding them up along with gold coins he currently has.

Now, into the test case we go:

 

Test Code


import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

static Stream<Arguments> shouldCountLootSource(){
	Treasure diamondTreasure = new Treasure(TreasureType.DIAMOND);
	Treasure emeraldTreasure = new Treasure(TreasureType.EMERALD);
	Treasure rubyTreasure = new Treasure(TreasureType.RUBY);

	List<Treasure> treasuresOne = Arrays.asList(
			new Treasure[]{diamondTreasure, emeraldTreasure});
	List<Treasure> treasuresTwo = Arrays.asList(
			new Treasure[]{diamondTreasure, emeraldTreasure, rubyTreasure});
	List<Treasure> treasuresThree = Arrays.asList(
			new Treasure[]{diamondTreasure, diamondTreasure, diamondTreasure});

	return Stream.of(
		arguments(treasuresOne, new Hero(1000), BigDecimal.valueOf(4500)),
		arguments(treasuresTwo, new Hero(0), BigDecimal.valueOf(4000)),
		arguments(treasuresThree, new Hero(1), BigDecimal.valueOf(7501))
	);
}

@ParameterizedTest
@MethodSource("shouldCountLootSource")
public void shouldCountLoot(List<Treasure> treasures, Hero hero,
							BigDecimal expectedLootWorth) throws Exception{
	// When
	BigDecimal lootWorth = gameControllerSUT.countLoot(treasures, hero);

	// Then
	assertThat(lootWorth)
			.isEqualByComparingTo(expectedLootWorth);
}

In this test method, we are:

        (L8-25):  Declaring a method source of data for our test. The source method has to always be static and return a Stream.

        (L28):  Making sure that the source method would be used. Also, it would be exactly the one named shouldCountLootSource.

        (L35):  Evaluating the calculated lootWorth with the passed expectedLootWorth for each of the provided cases.

 

Note
Note

 

Tip
Tip

 

6)     Conclusion

 

Summary

 

Having such a staple unit testing feature covered by Junit 5, we can rest assured that our tests have exhausted all the inputs that satisfy the given test case. We can also do it in an elegant, self documenting way. No loops nor a plethora of redundant tests anymore.

 

 

Need more insight?
Repository
Repository
Glossary
Glossary
Tags:
Reference
You may also like:
junit5-tags-and-filters
JUnit 5 tags and filters
junit5-ordered-tests
JUnit 5 ordered tests
Comments
Be the first to comment.
Leave a comment