В статьях о JUnit я использовал в качестве примеров простые самодостаточные классы, которые не имели никаких зависимостей. На практике такие классы встречаются редко: обычно класс, выполняющий хоть какие-либо действия, зависит от других классов. Примером может служить классическая архитектура с многослойными приложениями, когда один слой обращается (=зависит) от другого. Как тестировать такие объекты?
Можно попробовать создать все необходимые объекты и протестировать класс с ними. В некоторых условиях это даже может сработать 🙂 Но, во первых, это противоречит основам юнит-тестирования — тестирование отдельной части программы, а не отдельной части и всех её зависимостей. Во вторых цепочка зависимостей может вести в базу данных, какой-нибудь внешний сервис итд. В третьих, поведение этих зависимостей может быть в принципе непредсказуемым: что вернёт Random?
В примере знакомства со Spring при написании тестов были написаны и отдельные реализации зависимостей, написанные специально для тестов (stubs).
Такие дублёры позволяют подменять реальные объекты, используемые тестируемым классом, на их симуляцию, имеющую предсказуемое поведение. Вместо использования реальных объектов, реализующих зависимости тестируемого класса, используются упрощённые объекты, сделанные специально для теста.
В том же примере «Hello, Spring!» я написал для каждого интерфейса две реализации — основную (например CoinImpl) и тестовую (MockCoin). И скажу вам прямо: мне было крайне неохота писать тестовые реализации. А если бы интерфейсов в программе было не 3, а хотя бы 12, пришлось написать бы 24 их реализации. И что самое неприятное, если бы мне потребовалось разное поведение этих реализаций в разных тестах, пришлось бы писать ещё больше! Жуть.
EasyMock, наравне с другими фреймворками для создания тест-дублёров (а это, кстати, официальный термин, пропагандируемый аж самим Мартином Фаулером), автоматизирует и упрощает процесс создания тестовых реализаций интерфейсов.
Подготовка
Как обычно, начнём с пустого maven проекта:
1
2
3
4
|
>mvn archetype:generate -DgroupId=ru.easyjava.easymock -DartifactId=hello -Dversion=1 -DinteractiveMode=false
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
|
И добавим к нему, помимо обычных Junit и Hamcrest, ещё и EasyMock:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>4.12</junit.version>
<hamcrest.version>1.3</hamcrest.version>
<easymock.version>3.3.1</easymock.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>${easymock.version}</version>
</dependency>
</dependencies>
|
В заключении поместим туда же классы Coin, GreeterTarget и GreeterTargetImpl из примера Hello, Spring!
Самый простой mock
Код теста с использованием EasyMock настолько прост, что в нём даже нечего комментировать:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class GreeterTargetImplTest {
@Rule
public EasyMockRule em = new EasyMockRule(this);
@Mock(type = MockType.NICE)
private Coin coinMock;
@TestSubject
private GreeterTargetImpl testedObject = new GreeterTargetImpl(coinMock);
@Test
public void testTrue() {
expect(coinMock.toss()).andReturn(true);
replay(coinMock);
assertThat(testedObject.get(), is("World"));
}
@Test
public void testFalse() {
replay(coinMock);
assertThat(testedObject.get(), is("Spring"));
}
}
|
Разберём код теста подробно:
1
2
|
@Rule
public EasyMockRule em = new EasyMockRule(this);
|
EasyMockRule это правило JUnit, которое и реализует всю магию — обрабатывает переменные с аннотациями @Mock и @TestedObject.
1
2
|
@Mock
private Coin coinMock;
|
Каждая переменная с аннотацией @Mock обрабатывается правилом EasyMockRule и из неё создаётся mock объект. Состояние этого mock объекта будет сбрасываться к исходному автоматически, при вызове каждого теста.
1
2
|
@TestSubject
private GreeterTargetImpl testedObject = new GreeterTargetImpl(coinMock);
|
@TestSubject указывает EasyMock, какая из переменных класса является тестируемым классом. EasyMock использует эту информацию для автоматического связывания mock объектов с тестируемым объектом. Подробно вопросы связывания я рассмотрю в отдельной статье.
1
2
3
4
5
6
7
|
@Test
public void testTrue() {
expect(coinMock.toss()).andReturn(true);
replay(coinMock);
assertThat(testedObject.get(), is("World"));
}
|
Самый первый тест начинается с определения поведения mock объекта Coin: мы говорим, что будет вызван метод get() и что в ответ он должен вернуть true.
Посмотрите, насколько это проще, чем создание отдельной реализации интерфейса Coin, служащей только для тестов! Минимальная реализация такой заглушки требует как минимум дополнительной функции и хранит своё состояние:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class StubCoin implements Coin {
private boolean constantResult;
public final void setConstantResult(final boolean newResult) {
this.constantResult = newResult;
}
@Override
public boolean toss() {
return constantResult;
}
}
|
Вызов replay(coinMock) говорит EasyMock, что мы закончили с определением поведения mock объектов и дальнейшие вызовы должны воспроизводить заданное поведение.
Второй тест ещё короче:
1
2
3
4
5
|
@Test
public void testFalse() {
replay(coinMock);
assertThat(testedObject.get(), is("Spring"));
}
|
Здесь даже не определяется никакое поведение. Почему? Потому что в аннотации @Mock я указал, что хочу так называемый «nice mock». Nice mocks облегчают труд по созданию тестовых дублёров — все публичные методы nice mock возвращают значение по умолчанию, если их поведение не переопределено:
- 0 для чисел
- false для Boolean
- null для строк и прочих объектов
поэтому во втором тесте мы используем значение по умолчанию и пропускаем задание поведения.
Код примера доступен на github.