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