Статьи

Параметризованные тесты JUnit с помощью JUnitParams

Параметризованные модульные тесты используются для тестирования одного и того же кода в различных условиях. Благодаря параметризованным модульным тестам мы можем настроить метод тестирования, который извлекает данные из некоторого источника данных. Этот источник данных может быть набором объектов тестовых данных, внешним файлом или даже базой данных. Общая идея состоит в том, чтобы упростить тестирование различных условий одним и тем же методом модульного тестирования, что ограничит исходный код, который нам нужно написать, и сделает тестовый код более устойчивым. Мы можем назвать эти тесты управляемыми данными юнит-тестами. JUnit-логотип

Лучший способ выполнить управляемые данными модульные тесты в JUnit — это использовать пользовательский бегун JUnit — Parameterized или JUnitParamsRunner от JUnitParamsRunner . Использование подхода JUnit может работать во многих случаях, но последний кажется более простым в использовании и более мощным.

Основной пример

В нашем примере покерных кубиков нам нужно вычислить счет фулл-хауса. Как и в карточном покере, фулл-хаус — это бросок, в котором у вас есть как 3 вида, так и пара. Для простоты счет представляет собой сумму всех кубиков в броске. Итак, давайте посмотрим код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class FullHouse implements Scoreable {
 
    @Override
    public Score getScore(Collection dice) {
        Score pairScore = Scorables.pair().getScore(dice);
        Score threeOfAKindScore = Scorables.threeOfAKind().getScore(pairScore.getReminder());
        if (bothAreGreaterThanZero(pairScore.getValue(), threeOfAKindScore.getValue())) {
            return new Score(pairScore.getValue() + threeOfAKindScore.getValue()); // no reminder
        }
        return new Score(0, dice);
    }
 
    private boolean bothAreGreaterThanZero(int value1, int value2) {
        return value1 > 0 && value2 > 0;
    }
}

Я хотел бы быть уверен, что бросок правильно оценен (конечно, у меня уже есть юнит-тесты для Pair и ThreeOfAKind). Поэтому я хотел бы проверить следующие условия:

  • Счет 11 для: 1, 1, 3, 3, 3
  • Счет 8 для: 2, 2, 2, 1, 1
  • Счет 0 для: 2, 3, 4, 1, 1
  • Оценка 25 для: 5, 5, 5, 5, 5

Давайте исследуем два возможных способа написания управляемого данными теста для этого метода. Во-первых, параметризованный JUnit :

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
@RunWith(Parameterized.class)
public class FullHouseTest {
 
    private Collection rolled;
    private int score;
 
    public FullHouseTest(Collection rolled, int score) {
        this.rolled = rolled;
        this.score = score;
    }
 
    @Test
    public void fullHouse() {
        assertThat(new FullHouse().getScore(rolled).getValue()).isEqualTo(score);
    }
 
    @Parameterized.Parameters
    public static Iterable data() {
        return Arrays.asList(
                new Object[][]{
                        {roll(1, 1, 3, 3, 3), score(11)},
                        {roll(2, 2, 2, 1, 1), score(8)},
                        {roll(2, 3, 4, 1, 1), score(0)},
                        {roll(5, 5, 5, 5, 5), score(25)}
                }
        );
    }
 
    private static int score(int score) {
        return score;
    }
}

И другой, с JUnitParams :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(JUnitParamsRunner.class)
public class FullHouseTest {
 
    @Test
    @Parameters
    public void fullHouse(Collection rolled, int score) {
        assertThat(new FullHouse().getScore(rolled).getValue()).isEqualTo(score);
    }
 
    public Object[] parametersForFullHouse() {
        return $(
                $(roll(1, 1, 3, 3, 3), score(11)),
                $(roll(2, 2, 2, 1, 1), score(8)),
                $(roll(2, 3, 4, 1, 1), score(0)),
                $(roll(5, 5, 5, 5, 5), score(25))
        );
    }
 
    private static int score(int score) {
        return score;
    }
}

На первый взгляд оба выглядят очень похоже. И это правда. Так в чем же различие между JUnit Parameterized (1) и JUnitParams (2)? Наиболее важным из них является способ передачи параметров, а значит, и архитектура решения. В (1) параметры передаются в конструктор, тогда как в (2) параметры передаются непосредственно в метод тестирования. Должен ли я заботиться? Да. Одна из причин в том, что в (2) мы можем иметь несколько параметризованных методов тестирования с разными данными для каждого из методов, как в следующем примере:

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
@RunWith(JUnitParamsRunner.class)
public class NumberOfAKindTest {
 
    @Test
    @Parameters
    public void pair(Collection rolled, int[] expected, int score) {
        NumberOfAKind sut = new NumberOfAKind(2);
        doTest(rolled, expected, score, sut);
    }
 
    @Test
    @Parameters
    public void threeOfAKind(Collection rolled, int[] expected, int score) {
        NumberOfAKind sut = new NumberOfAKind(3);
        doTest(rolled, expected, score, sut);
    }
 
    public Object[] parametersForPair() {
        return $(
                $(roll(1, 1, 1, 2, 3), hand(1, 1), score(2)),
                $(roll(2, 1, 1, 1, 1), hand(1, 1), score(2)),
                $(roll(2, 3, 4, 1, 1), hand(1, 1), score(2)),
                $(roll(2, 3, 5, 5, 5), hand(5, 5), score(10)),
                $(roll(2, 1, 5, 4, 3), null, score(0))
        );
    }
 
    public Object[] parametersForThreeOfAKind() {
        return $(
                $(roll(1, 1, 1, 2, 3), hand(1, 1, 1), score(3)),
                $(roll(2, 1, 1, 1, 3), hand(1, 1, 1), score(3)),
                $(roll(2, 3, 1, 1, 1), hand(1, 1, 1), score(3)),
                $(roll(2, 3, 5, 5, 5), hand(5, 5, 5), score(15)),
                $(roll(2, 5, 5, 5, 6), hand(5, 5, 5), score(15)),
                $(roll(2, 2, 5, 5, 3), null, score(0))
        );
    }
 
    private static int[] hand(int... dice) {
        return dice;
    }
 
    private static int score(int score) {
        return score;
    }
 
}

В более простых примерах параметры могут быть определены как массив String непосредственно в аннотации @Parameters через метод value. Мы также можем извлечь данные во внешний класс и сделать наши тесты более чистыми и удобочитаемыми. Полный тест для NumberOfAKind следующим образом:

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
56
57
58
59
@RunWith(JUnitParamsRunner.class)
public class NumberOfAKindTest {
 
    @Test
    @Parameters(source = NumberOfAKindProvider.class, method = "providePair")
    public void pair(Collection rolled, int[] expected, int score) {
        NumberOfAKind sut = new NumberOfAKind(2);
        doTest(rolled, expected, score, sut);
    }
 
    @Test
    @Parameters(source = NumberOfAKindProvider.class, method = "provideThreeOfAKind")
    public void threeOfAKind(Collection rolled, int[] expected, int score) {
        NumberOfAKind sut = new NumberOfAKind(3);
        doTest(rolled, expected, score, sut);
    }
 
    @Test
    @Parameters(source = NumberOfAKindProvider.class, method = "provideFourOfAKind")
    public void fourOfAKind(Collection rolled, int[] expected, int score) {
        NumberOfAKind sut = new NumberOfAKind(4);
        doTest(rolled, expected, score, sut);
    }
 
    @Test
    @Parameters(source = NumberOfAKindProvider.class, method = "provideFiveOfAKind")
    public void fiveOfAKind(Collection rolled, int[] expected, int score) {
        NumberOfAKind sut = new NumberOfAKind(5);
        doTest(rolled, expected, score, sut);
    }
 
    private void doTest(Collection rolled, int[] expected, int score, NumberOfAKind sut) {
        Collection consecutiveDice = sut.getConsecutiveDice(rolled);
 
        assertDiceContainsValues(consecutiveDice, expected);
        assertThat(sut.getScore(rolled).getValue()).isEqualTo(score);
    }
 
    private void assertDiceContainsValues(Collection dice, int[] expected) {
        Collection values = toInts(dice);
        if (expected == null) {
            assertThat(values).isEmpty();
            return;
        }
        for (int i = 0; i < expected.length; i++) {
            assertThat(values).hasSize(expected.length).contains(expected[i]);
        }
    }
 
    private Collection toInts(Collection dice) {
        return Collections2.transform(dice, new Function() {
            @Override
            public Integer apply(Dice input) {
                return input.getValue();
            }
        });
    }
 
}

Каждый метод определяет класс поставщика и имя метода поставщика. Посмотрите на провайдера ниже:

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
public class NumberOfAKindProvider {
 
    public static Object[] providePair() {
        return $(
                $(roll(1, 1, 1, 2, 3), hand(1, 1), score(2)),
                $(roll(2, 1, 1, 1, 1), hand(1, 1), score(2)),
                $(roll(2, 3, 4, 1, 1), hand(1, 1), score(2)),
                $(roll(2, 3, 5, 5, 5), hand(5, 5), score(10)),
                $(roll(2, 1, 5, 4, 3), null, score(0))
        );
    }
 
    public static Object[] provideThreeOfAKind() {
        return $(
                $(roll(1, 1, 1, 2, 3), hand(1, 1, 1), score(3)),
                $(roll(2, 1, 1, 1, 3), hand(1, 1, 1), score(3)),
                $(roll(2, 3, 1, 1, 1), hand(1, 1, 1), score(3)),
                $(roll(2, 3, 5, 5, 5), hand(5, 5, 5), score(15)),
                $(roll(2, 5, 5, 5, 6), hand(5, 5, 5), score(15)),
                $(roll(2, 2, 5, 5, 3), null, score(0))
        );
    }
 
    public static Object[] provideFourOfAKind() {
        return $(
                $(roll(1, 1, 1, 1, 3), hand(1, 1, 1, 1), score(4)),
                $(roll(2, 1, 1, 1, 1), hand(1, 1, 1, 1), score(4)),
                $(roll(2, 5, 5, 5, 5), hand(5, 5, 5, 5), score(20)),
                $(roll(2, 3, 4, 5, 5), null, score(0))
        );
    }
 
    public static Object[] provideFiveOfAKind() {
        return $(
                $(roll(1, 1, 1, 1, 1), hand(1, 1, 1, 1, 1), score(5)),
                $(roll(6, 6, 6, 6, 6), hand(6, 6, 6, 6, 6), score(30)),
                $(roll(6, 6, 6, 6), null, score(0)),
                $(roll(2, 3, 4, 6, 6), null, score(0))
        );
    }
 
    private static int[] hand(int... dice) {
        return dice;
    }
 
    private static int score(int score) {
        return score;
    }
}

Резюме

Для меня JUnitParams — лучшее решение для написания хороших модульных тестов, управляемых данными. Но то, что представлено выше, это еще не все, что библиотека может предложить разработчику. Есть больше возможностей. Параметры могут быть переданы в виде строки CSV, мы можем смешивать как параметризованные, так и непараметрические тесты, чтобы упомянуть несколько.

Посетите веб-сайт проекта, чтобы узнать больше об этой библиотеке: https://code.google.com/p/junitparams