Статьи

Сотрудничество JUnit и EasyMock

Разработчики всегда должны заботиться о коде, который они произвели. Они должны убедиться, что код работает правильно после того, как была реализована новая функция или исправлена ​​какая-то ошибка. Этого можно добиться хотя бы с помощью модульных тестов. Поскольку этот блог посвящен языку программирования Java, сегодня я напишу статью о средах JUnit 4.1 и EasyMock 3.1 . Основная цель этих фреймворков — облегчить написание модульных тестов.

Вступление

Для демонстрации возможностей JUnit и EasyMock мне нужно иметь некоторый код, который я собираюсь покрыть тестами. Итак, в начале я собираюсь представить вам простой

приложение, которое я разработал для этого урока. Он должен быть очень знаком для разработчиков Java, потому что он эмулирует работу кофемашины. Теперь я опишу в нескольких словах функциональность приложения. Кофемашина имеет две емкости: для воды и для кофе. Вы можете сделать три сорта кофе в зависимости от размера порции (маленький, средний, большой).

  • Позитивный сценарий: вы делаете порцию кофе, а в контейнерах достаточно воды и кофейных зерен.
  • Негативный сценарий: вы делаете порцию кофе, а в контейнерах не хватает воды или кофейных зерен.

Если какой-либо контейнер не укомплектован, вы можете пополнить его. После краткого описания функциональности приложения вы можете легко представить, как оно должно работать. Пришло время предоставить примеры кода классов.

Код приложения

Теперь постарайтесь быть более терпеливым и внимательным. Ниже вы увидите множество классов, которые я собираюсь проверить в следующем параграфе. Как упоминалось выше, кофемашина может производить три вида кофе в зависимости от размера порции, которую вы хотите получить. Части в приложении будут представлены в виде перечисления.

01
02
03
04
05
06
07
08
09
10
11
12
13
public enum Portion {
    SMALL(1), MEDIUM(2), LARGE(3);
 
    private int size;
 
    private Portion(int size) {
        this.size = size;
    }
 
    public int size() {
        return size;
    }
}

Наша кофемашина имеет два контейнера, а это значит, что нам нужно построить логическую архитектуру для них. Давайте придерживаться подходов умного кодирования на основе интерфейсов.

1
2
3
4
5
6
7
8
public interface IContainer {
 
    public boolean getPortion(Portion portion) throws NotEnoughException;
    public int getCurrentVolume();
    public int getTotalVolume();
    public void refillContainer();
 
}

Чтобы избежать дублирования кода, мне нужно разработать абстрактный класс для контейнера. В контексте этого подхода к программированию я хочу вспомнить один из моих постов об абстрактном классе VS Interface .

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
public abstract class AbstractContainer implements IContainer {
 
    private int containerTotalVolume;
    private int currentVolume;
 
    public AbstractContainer(int volume) {
        if (volume < 1)
            throw new IllegalArgumentException('Container's value must be greater then 0.');
        containerTotalVolume = volume;
        currentVolume = volume;
    }
 
    @Override
    public boolean getPortion(Portion portion) throws NotEnoughException {
        int delta = currentVolume - portion.size();
        if (delta > -1) {
            currentVolume -= portion.size();
            return true;
        } else
            throw new NotEnoughException('Refill the '
                    + this.getClass().getName());
    }
 
    @Override
    public int getCurrentVolume() {
        return currentVolume;
    }
 
    @Override
    public int getTotalVolume() {
        return containerTotalVolume;
    }
 
    @Override
    public void refillContainer() {
        currentVolume = containerTotalVolume;
    }
 
}

Методы в абстрактном контейнере самоочевидны, поэтому нет необходимости останавливаться на них более подробно. Вы, наверное, заметили NotEnoughException, не волнуйтесь, в этом нет ничего особенного, только пользовательское исключение для приложения.

1
2
3
4
5
6
7
public class NotEnoughException extends Exception {
 
    public NotEnoughException(String text) {
        super(text);
    }
 
}

После завершения разработки интерфейса контейнера и абстрактного класса мы можем приступить к реализации конкретного контейнера.

1
2
3
4
5
6
7
public class CoffeeContainer extends AbstractContainer {
 
    public CoffeeContainer(int volume) {
        super(volume);
    }
 
}

Тот же класс будет для контейнера для воды:

1
2
3
4
5
6
7
public class WaterContainer extends AbstractContainer {
 
    public WaterContainer(int volume) {
        super(volume);
    }
 
}

Теперь у нас есть все необходимое для разработки кода, связанного с кофемашиной. Как и раньше, я начну с разработки интерфейса.

1
2
3
4
5
6
7
public interface ICoffeeMachine {
 
    public boolean makeCoffee(Portion portion) throws NotEnoughException;
    public IContainer getCoffeeContainer();
    public IContainer getWaterContainer();
 
}

И, наконец, вот реализация кофемашины:

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
public class CoffeeMachine implements ICoffeeMachine {
 
    private IContainer coffeeContainer;
    private IContainer waterContainer;
 
    public CoffeeMachine(IContainer cContainer, IContainer wContainer) {
        coffeeContainer = cContainer;
        waterContainer = wContainer;
    }
 
    @Override
    public boolean makeCoffee(Portion portion) throws NotEnoughException {
 
        boolean isEnoughCoffee = coffeeContainer.getPortion(portion);
        boolean isEnoughWater = waterContainer.getPortion(portion);
 
        if (isEnoughCoffee && isEnoughWater) {
            return true;
        } else {
            return false;
        }
    }
 
    @Override
    public IContainer getWaterContainer() {
        return waterContainer;
    }
 
    @Override
    public IContainer getCoffeeContainer() {
        return coffeeContainer;
    }
 
}

Вот и все, что относится к программе, которую я собираюсь протестировать с помощью модульных тестов.

JUnit тестирование

Прежде чем начать разработку тестов JUnit, я хочу повторить канонические цели модульных тестов. Модульный тест проверяет наименьшую часть функциональности — метод или класс. Это обстоятельство накладывает некоторые логические ограничения на развитие. Это означает, что вам не нужно вводить какую-то дополнительную логику в метод, потому что после этого это становится более трудным для тестирования. И еще одна важная вещь — модульное тестирование подразумевает изоляцию функциональности от других частей приложения. Нам не нужно проверять функциональность метода «A», пока мы работаем с методом «B». Итак, давайте напишем стат для написания тестов JUnit для приложения кофемашины. Для этого нам нужно добавить некоторые зависимости в pom.xml

01
02
03
04
05
06
07
08
09
10
11
12
...
        <dependency>
            <groupid>org.easymock</groupid>
            <artifactid>easymock</artifactid>
            <version>3.1</version>
        </dependency>
        <dependency>
            <groupid>junit</groupid>
            <artifactid>junit</artifactid>
            <version>4.11</version>
        </dependency>
...

Я выбрал класс AbstractContainer для демонстрации тестов JUnit. Потому что в контексте приложения у нас есть две реализации этого класса, и если мы напишем тесты для него, автоматически мы будем тестировать класс WaterContainer и класс CoffeeContainer.

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import static org.junit.Assert.assertEquals;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
import com.app.data.Portion;
import com.app.exceptions.NotEnoughException;
import com.app.mechanism.WaterContainer;
import com.app.mechanism.interfaces.IContainer;
 
public class AbstractContainerTest {
 
    IContainer waterContainer;
    private final static int VOLUME = 10;
 
    @Before
    public void beforeTest() {
        waterContainer = new WaterContainer(VOLUME);
    }
 
    @After
    public void afterTest() {
        waterContainer = null;
    }
 
    @Test(expected = IllegalArgumentException.class)
    public void testAbstractContainer() {
        waterContainer = new WaterContainer(0);
    }
 
    @Test
    public void testGetPortion() throws NotEnoughException {
        int expCurVolume = VOLUME;
 
        waterContainer.getPortion(Portion.SMALL);
        expCurVolume -= Portion.SMALL.size();
        assertEquals('Calculation for the SMALL portion is incorrect',
                expCurVolume, waterContainer.getCurrentVolume());
 
        waterContainer.getPortion(Portion.MEDIUM);
        expCurVolume -= Portion.MEDIUM.size();
        assertEquals('Calculation for the MEDIUM portion is incorrect',
                expCurVolume, waterContainer.getCurrentVolume());
 
        waterContainer.getPortion(Portion.LARGE);
        expCurVolume -= Portion.LARGE.size();
        assertEquals('Calculation for the LARGE portion is incorrect',
                expCurVolume, waterContainer.getCurrentVolume());
 
    }
 
    @Test(expected = NotEnoughException.class)
    public void testNotEnoughException() throws NotEnoughException {
        waterContainer.getPortion(Portion.LARGE);
        waterContainer.getPortion(Portion.LARGE);
        waterContainer.getPortion(Portion.LARGE);
        waterContainer.getPortion(Portion.LARGE);
    }
 
    @Test
    public void testGetCurrentVolume() {
        assertEquals('Current volume has incorrect value.', VOLUME,
                waterContainer.getCurrentVolume());
    }
 
    @Test
    public void testGetTotalVolume() {
        assertEquals('Total volume has incorrect value.', VOLUME,
                waterContainer.getTotalVolume());
    }
 
    @Test
    public void testRefillContainer() throws NotEnoughException {
        waterContainer.getPortion(Portion.SMALL);
        waterContainer.refillContainer();
        assertEquals('Refill functionality works incorectly.', VOLUME,
                waterContainer.getCurrentVolume());
    }
 
}

Мне нужно объяснить, для чего используются все аннотации. Но я ленив для этого и просто дам вам ссылку на JUnit API . Там вы можете прочитать самые правильные объяснения. Обратите внимание на общие вещи для всех тестов — все они помечены аннотацией @Test, это указывает на то, что следующий метод является тестом, и каждый тест заканчивается некоторыми из методов assert . Утверждения являются неотъемлемой частью каждого теста, потому что все манипуляции в тесте должны быть проверены в конце.

JUnit с тестированием EasyMock

Хорошо, в предыдущем параграфе я покажу вам пример нескольких простых тестов JUnit. В этом примере тесты не взаимодействуют с другими классами. Что если нам нужно задействовать какой-то дополнительный класс в тесте JUnit? Я упоминал выше, что модульные тесты должны быть изолированы от функциональности остальных приложений. Для этого вы можете использовать тестовый фреймворк EasyMock . С помощью EasyMock вы можете создавать макеты. Мок — это объекты, которые имитируют поведение реального конкретного объекта, но с одним большим плюсом вы можете указать состояние для макета, и таким образом вы получите то состояние для фальшивого объекта, которое вам необходимо в конкретный момент модульного тестирования.

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
import static org.junit.Assert.*;
 
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
public class CoffeeMachineTest {
 
    ICoffeeMachine coffeeMachine;
    IContainer coffeeContainer;
    IContainer waterContainer;
 
    @Before
    public void setUp() {
        coffeeContainer = EasyMock.createMock(CoffeeContainer.class);
        waterContainer = EasyMock.createMock(WaterContainer.class);
        coffeeMachine = new CoffeeMachine(coffeeContainer, waterContainer);
    }
 
    @After
    public void tearDown() {
        coffeeContainer = null;
        waterContainer = null;
        coffeeMachine = null;      
    }
 
    @Test
    public void testMakeCoffe() throws NotEnoughException {
        EasyMock.expect(coffeeContainer.getPortion(Portion.LARGE)).andReturn(true);
        EasyMock.replay(coffeeContainer);
 
        EasyMock.expect(waterContainer.getPortion(Portion.LARGE)).andReturn(true);
        EasyMock.replay(waterContainer);
 
        assertTrue(coffeeMachine.makeCoffee(Portion.LARGE));
    }
 
    @Test
    public void testNotEnoughException() throws NotEnoughException {
        EasyMock.expect(coffeeContainer.getPortion(Portion.LARGE)).andReturn(false);
        EasyMock.replay(coffeeContainer);
 
        EasyMock.expect(waterContainer.getPortion(Portion.LARGE)).andReturn(true);
        EasyMock.replay(waterContainer);
 
        assertFalse(coffeeMachine.makeCoffee(Portion.LARGE));
    }
 
}

В предыдущем фрагменте кода вы видите сотрудничество JUnit и EasyMock. Я могу подчеркнуть несколько фундаментальных вещей в использовании EasyMock.

  1. Если тестирование требует взаимодействия с каким-либо внешним объектом, вы должны поиздеваться над ним.
    1
    coffeeContainer = EasyMock.createMock(CoffeeContainer.class);
  2. Установите поведение для макета или для конкретного метода, который требуется для тестирования тестируемого объекта.
    1
    EasyMock.expect(coffeeContainer.getPortion(Portion.LARGE)).andReturn(true);
  3. Переключить макет в режим ответа.
    1
    EasyMock.replay(coffeeContainer);

    EasyMock имеет много различных методов, его API не маленький, поэтому я рекомендую прочитать больше на официальном сайте .

Тестовый набор JUnit

Когда у вас небольшое приложение, вы можете запускать тесты JUnit отдельно, но что, если вы работаете с большим и сложным приложением? В этом случае модульные тесты могут быть объединены в тестовые наборы по некоторой функции. JUnit предоставляет удобный способ для этого.

1
2
3
4
5
@RunWith(Suite.class)
@SuiteClasses({ AbstractContainerTest.class, CoffeeMachineTest.class })
public class AllTests {
 
}

Резюме

Модульное тестирование — это очень важная часть разработки программного обеспечения, в нем много подходов, методологий и инструментов. В этом посте я сделал обзор JUnit и EasyMock, но я опустил много интересных моментов и техник, которые я планирую осветить в следующих уроках. Вы можете скачать исходный код руководства с моего DropBox.

Ссылка: сотрудничество JUnit и EasyMock от нашего партнера по JCG Алекса Фрузенштейна в блоге Фрузенштейна .