1. Обзор
В этом руководстве мы проанализируем различные варианты использования и возможные альтернативные решения для модульного тестирования абстрактных классов с помощью неабстрактных методов.
Обратите внимание, что тестирование абстрактных классов почти всегда должно проходить через общедоступный API конкретных реализаций , поэтому не применяйте описанные ниже методы, если вы не уверены в том, что делаете.
2. Зависимости Maven
Начнем с зависимостей Maven:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.8.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.4</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
Последние версии этих библиотек вы можете найти на Maven Central .
Powermock не полностью поддерживается для Junit5. Кроме того, powermock-module-junit4
используется только для одного примера, представленного в разделе 5.
3. Независимый неабстрактный метод
Давайте рассмотрим случай, когда у нас есть абстрактный класс с публичным неабстрактным методом:
public abstract class AbstractIndependent {
public abstract int abstractFunc();
public String defaultImpl() {
return "DEFAULT-1";
}
}
Мы хотим протестировать метод defaultImpl()
, и у нас есть два возможных решения — использование конкретного класса или использование Mockito.
3.1. Использование конкретного класса
Создайте конкретный класс, который расширяет класс AbstractIndependent
, и используйте его для тестирования метода:
public class ConcreteImpl extends AbstractIndependent {
@Override
public int abstractFunc() {
return 4;
}
}
@Test
public void givenNonAbstractMethod_whenConcreteImpl_testCorrectBehaviour() {
ConcreteImpl conClass = new ConcreteImpl();
String actual = conClass.defaultImpl();
assertEquals("DEFAULT-1", actual);
}
Недостатком этого решения является необходимость создания конкретного класса с фиктивными реализациями всех абстрактных методов.
3.2. Использование Мокито
В качестве альтернативы мы можем использовать Mockito
для создания макета:
@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
AbstractIndependent absCls = Mockito.mock(
AbstractIndependent.class,
Mockito.CALLS_REAL_METHODS);
assertEquals("DEFAULT-1", absCls.defaultImpl());
}
Наиболее важной частью здесь является подготовка макета для использования реального кода, когда метод вызывается с использованием Mockito.CALLS_REAL_METHODS
.
4. Абстрактный метод, вызываемый из неабстрактного метода
В этом случае неабстрактный метод определяет глобальный поток выполнения, а абстрактный метод может быть написан по-разному в зависимости от варианта использования:
public abstract class AbstractMethodCalling {
public abstract String abstractFunc();
public String defaultImpl() {
String res = abstractFunc();
return (res == null) ? "Default" : (res + " Default");
}
}
Чтобы протестировать этот код, мы можем использовать те же два подхода, что и раньше — либо создать конкретный класс, либо использовать Mockito для создания макета:
@Test
public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() {
AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
Mockito.when(cls.abstractFunc())
.thenReturn("Abstract");
Mockito.doCallRealMethod()
.when(cls)
.defaultImpl();
assertEquals("Abstract Default", cls.defaultImpl());
}
Здесь abstractFunc()
заглушается возвращаемым значением, которое мы предпочитаем для теста. Это означает, что когда мы вызываем неабстрактный метод defaultImpl()
, он будет использовать эту заглушку.
5. Неабстрактный метод с тестовой преградой
В некоторых сценариях метод, который мы хотим протестировать, вызывает закрытый метод, который содержит тестовое препятствие.
Нам нужно обойти мешающий тестовый метод перед тестированием целевого метода:
public abstract class AbstractPrivateMethods {
public abstract int abstractFunc();
public String defaultImpl() {
return getCurrentDateTime() + "DEFAULT-1";
}
private String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
В этом примере метод defaultImpl()
вызывает закрытый метод getCurrentDateTime()
. Этот частный метод получает текущее время во время выполнения, чего следует избегать в наших модульных тестах.
Теперь, чтобы поиздеваться над стандартным поведением этого приватного метода, мы даже не можем использовать Mockito,
потому что он не может контролировать приватные методы.
Вместо этого нам нужно использовать PowerMock ( обратите внимание , что этот пример работает только с JUnit 4, поскольку поддержка этой зависимости недоступна для JUnit 5 ):
@RunWith(PowerMockRunner.class)
@PrepareForTest(AbstractPrivateMethods.class)
public class AbstractPrivateMethodsUnitTest {
@Test
public void whenMockPrivateMethod_thenVerifyBehaviour() {
AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class);
PowerMockito.doCallRealMethod()
.when(mockClass)
.defaultImpl();
String dateTime = LocalDateTime.now().toString();
PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime");
String actual = mockClass.defaultImpl();
assertEquals(dateTime + "DEFAULT-1", actual);
}
}
Важные биты в этом примере:
@RunWith
определяет PowerMock как бегун для теста@PrepareForTest(class)
указывает PowerMock подготовить класс для последующей обработки
Интересно, что мы просим PowerMock заглушить
закрытый метод getCurrentDateTime().
PowerMock будет использовать отражение, чтобы найти его, потому что он недоступен извне.
Итак ,
когда мы вызываем defaultImpl()
, заглушка, созданная для частного метода, будет вызываться вместо фактического метода.
6. Неабстрактный метод, который обращается к полям экземпляра
Абстрактные классы могут иметь внутреннее состояние, реализованное с помощью полей класса. Значение полей может существенно повлиять на тестируемый метод.
Если поле является общедоступным или защищенным, мы можем легко получить к нему доступ из тестового метода.
Но если это приват, мы должны использовать PowerMockito
:
public abstract class AbstractInstanceFields {
protected int count;
private boolean active = false;
public abstract int abstractFunc();
public String testFunc() {
if (count > 5) {
return "Overflow";
}
return active ? "Added" : "Blocked";
}
}
Здесь метод testFunc() использует
количество
полей уровня экземпляра и активен
перед возвратом.
При тестировании testFunc()
мы можем изменить значение поля count
, обратившись к экземпляру, созданному с помощью Mockito.
С другой стороны, чтобы протестировать поведение с закрытым активным
полем, нам снова придется использовать PowerMockito
и его класс Whitebox
:
@Test
public void whenPowerMockitoAndActiveFieldTrue_thenCorrectBehaviour() {
AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class);
PowerMockito.doCallRealMethod()
.when(instClass)
.testFunc();
Whitebox.setInternalState(instClass, "active", true);
assertEquals("Added", instClass.testFunc());
}
Мы создаем класс-заглушку, используя PowerMockito.mock()
, и мы используем класс Whitebox
для управления внутренним состоянием объекта.
Значение активного
поля меняется на true
.
7. Заключение
В этом руководстве мы видели несколько примеров, которые охватывают множество вариантов использования. Мы можем использовать абстрактные классы во многих других сценариях в зависимости от выбранного дизайна.
Кроме того, написание модульных тестов для методов абстрактного класса так же важно, как и для обычных классов и методов. Мы можем протестировать каждый из них, используя различные методы или доступные библиотеки поддержки тестирования.
Полный исходный код доступен на GitHub .