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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | 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):
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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.
What is your opinion?
Resources
- https://github.com/joel-costigliola/assertj-core/wiki/Creating-specific-assertions
- The evolution of assertions via @tkaczanowski
Reference: | Spice up your test code with custom assertions from our JCG partner Rafal Borowiec at the Codeleak.pl blog. |