Статьи

Приправьте свой тестовый код с помощью пользовательских утверждений

Вдохновленный выступлением @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. Они улучшат читабельность моего кода в некоторых случаях. С другой стороны, я почти уверен, что их нельзя использовать во всех сценариях. Особенно в тех, где вероятность повторного использования минимальна. В таком случае могут использоваться частные методы с сгруппированными утверждениями.

Каково твое мнение?

Ресурсы