Вдохновленный выступлением @tkaczanowski во время конференции GeeCON, я решил поближе познакомиться с пользовательскими утверждениями в библиотеке AssertJ .
В моей игре «Кости» я создал «Шанс», представляющий собой любую комбинацию костей с количеством очков, рассчитанным как сумма всех костей. Это относительно простой объект:
|
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);} |
В своем тесте я хотел посмотреть, как рассчитывается оценка для разных комбинаций костей. Я начал с простого (и только одного на самом деле):
|
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); }} |
Единственное понятие — объект оценки — проверяется в тесте. Чтобы улучшить читаемость и возможность повторного использования проверки баллов, я создам собственное утверждение. Я хотел бы, чтобы мое утверждение использовалось как любое другое утверждение AssertJ следующим образом:
|
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); }} |
Для этого мне нужно создать класс ScoreAssertion который расширяется от org.assertj.core.api.AbstractAssert . Класс должен иметь открытый статический метод фабрики и все необходимые методы проверки. В итоге реализация может выглядеть так, как показано ниже.
|
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; }} |
Мотивация создания такого утверждения — иметь более читаемый и многократно используемый код. Но это идет с определенной ценой — нужно создавать больше кода. В моем примере я знаю, что довольно скоро создам больше Scorables и мне нужно будет проверить алгоритм их оценки, поэтому создание дополнительного кода оправдано. Усиление будет видно. Например, я создал класс NumberInARow который вычисляет оценку для всех последовательных чисел в данной комбинации костей. Счет представляет собой сумму всех кубиков с заданным значением:
|
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(); }} |
Я начал с теста, который проверяет две пятерки подряд, и я уже пропустил утверждение — hasReminder — поэтому я улучшил ScoreAssertion . Я продолжал изменять утверждение в других тестах, пока не получил достаточно DSL хорошей формы, который я могу использовать в своих тестах:
|
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)); }} |
Утверждение после изменений выглядит следующим образом:
|
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; }} |
Мне очень нравится идея пользовательских утверждений AssertJ. Они улучшат читабельность моего кода в некоторых случаях. С другой стороны, я почти уверен, что их нельзя использовать во всех сценариях. Особенно в тех, где вероятность повторного использования минимальна. В таком случае могут использоваться частные методы с сгруппированными утверждениями.
Каково твое мнение?
Ресурсы
- https://github.com/joel-costigliola/assertj-core/wiki/Creating-specific-assertions
- Эволюция утверждений через @tkaczanowski
| Ссылка: | Приправьте свой тестовый код специальными утверждениями от нашего партнера JCG Рафаля Боровца в блоге Codeleak.pl . |