Spice up your test code with custom assertions
Inspired by the @tkaczanowski talk during GeeCON conference I decided to have a closer look at custom assertions with AssertJ library.
In my 'Dice' game I created a 'Chance' that is any combination of dice with the score calculated as a sum of all dice. This is relatively simple object:
class Chance implements Scorable {
@Override
public Score getScore(Collection<Dice> dice) {
int sum = dice.stream()
.mapToInt(die -> die.getValue())
.sum();
return scoreBuilder(this)
.withValue(sum)
.withCombination(dice)
.build();
}
}
public interface Scorable {
Score getScore(Collection<Dice> dice);
}
In my test I wanted to see how the score is calculated for different dice combination. I started with simple (and only one actually):
public class ChanceTest {
private Chance chance = new Chance();
@Test
@Parameters
public void chance(Collection<Dice> rolled, int scoreValue) {
// arrange
Collection<Dice> rolled = dice(1, 1, 3, 3, 3);
// act
Score score = chance.getScore(rolled);
// assert
assertThat(actualScore.getScorable()).isNotNull();
assertThat(actualScore.getValue()).isEqualTo(expectedScoreValue);
assertThat(actualScore.getReminder()).isEmpty();
assertThat(actualScore.getCombination()).isEqualTo(rolled);
}
}
A single concept - score object - is validated in the test. To improve the readability and reusability of the score validation I will create a custom assertion. I would like my assertion is used like any other AssertJ assertion as follows:
public class ChanceTest {
private Chance chance = new Chance();
@Test
public void scoreIsSumOfAllDice() {
Collection<Dice> rolled = dice(1, 1, 3, 3, 3);
Score score = chance.getScore(rolled);
ScoreAssertion.assertThat(score)
.hasValue(11)
.hasNoReminder()
.hasCombination(rolled);
}
}
In order to achieve that I need to create a ScoreAssertion class that extends from org.assertj.core.api.AbstractAssert. The class should have a public static factory method and all the needed verification methods. In the end, the implementation may look like the below one.
class ScoreAssertion extends AbstractAssert<ScoreAssertion, Score> {
protected ScoreAssertion(Score actual) {
super(actual, ScoreAssertion.class);
}
public static ScoreAssertion assertThat(Score actual) {
return new ScoreAssertion(actual);
}
public ScoreAssertion hasEmptyReminder() {
isNotNull();
if (!actual.getReminder().isEmpty()) {
failWithMessage("Reminder is not empty");
}
return this;
}
public ScoreAssertion hasValue(int scoreValue) {
isNotNull();
if (actual.getValue() != scoreValue) {
failWithMessage("Expected score to be <%s>, but was <%s>",
scoreValue, actual.getValue());
}
return this;
}
public ScoreAssertion hasCombination(Collection<Dice> expected) {
Assertions.assertThat(actual.getCombination())
.containsExactly(expected.toArray(new Dice[0]));
return this;
}
}
The motivation of creating such an assertion is to have more readable and reusable code. But it comes with some price - more code needs to be created. In my example, I know I will create more Scorables quite soon and I will need to verify their scoring algorithm, so creating an additional code is justified. The gain will be visible. For example, I created a NumberInARow class that calculates the score for all consecutive numbers in a given dice combination. The score is a sum of all dice with the given value:
class NumberInARow implements Scorable {
private final int number;
public NumberInARow(int number) {
this.number = number;
}
@Override
public Score getScore(Collection<Dice> dice) {
Collection<Dice> combination = dice.stream()
.filter(value -> value.getValue() == number)
.collect(Collectors.toList());
int scoreValue = combination
.stream()
.mapToInt(value -> value.getValue())
.sum();
Collection<Dice> reminder = dice.stream()
.filter(value -> value.getValue() != number)
.collect(Collectors.toList());
return Score.scoreBuilder(this)
.withValue(scoreValue)
.withReminder(reminder)
.withCombination(combination)
.build();
}
}
I started with the test that checks a two fives in a row and I already missed on assertion - hasReminder - so I improved the ScoreAssertion. I continued with changing the assertion with other tests until I got quite well shaped DSL I can use in my tests:
public class NumberInARowTest {
@Test
public void twoFivesInARow() {
NumberInARow numberInARow = new NumberInARow(5);
Collection<Dice> dice = dice(1, 2, 3, 4, 5, 5);
Score score = numberInARow.getScore(dice);
// static import ScoreAssertion
assertThat(score)
.hasValue(10)
.hasCombination(dice(5, 5))
.hasReminder(dice(1, 2, 3, 4));
}
@Test
public void noNumbersInARow() {
NumberInARow numberInARow = new NumberInARow(5);
Collection<Dice> dice = dice(1, 2, 3);
Score score = numberInARow.getScore(dice);
assertThat(score)
.isZero()
.hasReminder(dice(1, 2, 3));
}
}
public class TwoPairsTest {
@Test
public void twoDistinctPairs() {
TwoPairs twoPairs = new TwoPairs();
Collection<Dice> dice = dice(2, 2, 3, 3, 1, 4);
Score score = twoPairs.getScore(dice);
assertThat(score)
.hasValue(10)
.hasCombination(dice(2, 2, 3, 3))
.hasReminder(dice(1, 4));
}
}
The assertion after changes looks as follows:
class ScoreAssertion extends AbstractAssert<ScoreAssertion, Score> {
protected ScoreAssertion(Score actual) {
super(actual, ScoreAssertion.class);
}
public static ScoreAssertion assertThat(Score actual) {
return new ScoreAssertion(actual);
}
public ScoreAssertion isZero() {
hasValue(Score.ZERO);
hasNoCombination();
return this;
}
public ScoreAssertion hasValue(int scoreValue) {
isNotNull();
if (actual.getValue() != scoreValue) {
failWithMessage("Expected score to be <%s>, but was <%s>",
scoreValue, actual.getValue());
}
return this;
}
public ScoreAssertion hasNoReminder() {
isNotNull();
if (!actual.getReminder().isEmpty()) {
failWithMessage("Reminder is not empty");
}
return this;
}
public ScoreAssertion hasReminder(Collection<Dice> expected) {
isNotNull();
Assertions.assertThat(actual.getReminder())
.containsExactly(expected.toArray(new Dice[0]));
return this;
}
private ScoreAssertion hasNoCombination() {
isNotNull();
if (!actual.getCombination().isEmpty()) {
failWithMessage("Combination is not empty");
}
return this;
}
public ScoreAssertion hasCombination(Collection<Dice> expected) {
isNotNull();
Assertions.assertThat(actual.getCombination())
.containsExactly(expected.toArray(new Dice[0]));
return this;
}
}
I really like the idea of custom AssertJ assertions. They will improve the readability of my code in certain cases. On the other hand, I am pretty sure they cannot be used in all scenarios. Especially in those, where the chance of reusability is minimal. In such a case private methods with grouped assertions can be used. References
- https://github.com/joel-costigliola/assertj-core/wiki/Creating-specific-assertions
- The evolution of assertions via @tkaczanowski