Тестирование абстрактного класса с помощью JUnit

Тестирование абстрактного класса с помощью JUnit

1. обзор

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

Обратите внимание наtesting abstract classes should almost always go through the public API of the concrete implementations, поэтому не применяйте описанные ниже методы, если вы не уверены, что делаете.

2. Maven Зависимости

Начнем с зависимостей Maven:


    org.junit.jupiter
    junit-jupiter-engine
    5.1.0
    test


    org.mockito
    mockito-core
    2.8.9
    test


    org.powermock
    powermock-module-junit4
    1.7.4
    test
    
        
            junit
            junit
        
    


    org.powermock
    powermock-api-mockito2
    1.7.4
    test

Вы можете найти последние версии этих библиотек на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

В качестве альтернативы мы можем использоватьMockito to для создания макета:

@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
    AbstractIndependent absCls = Mockito.mock(
      AbstractIndependent.class,
      Mockito.CALLS_REAL_METHODS);

    assertEquals("DEFAULT-1", absCls.defaultImpl());
}

Самая важная часть здесь -preparation of the mock to use the real code when a method is invoked с использованием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 (note that this example works only with JUnit 4 because support for this dependency isn’t available for 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 будет использовать отражение, чтобы найти его, потому что он недоступен извне.

Итак,w, когда мы вызываем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() перед возвратом использует поля уровня экземпляраcount иactive .

При тестированииtestFunc() мы можем изменить значение поляcount, обратившись к экземпляру, созданному с помощьюMockito. 

С другой стороны, чтобы проверить поведение с закрытым полемactive, нам снова придется использовать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 для управления внутренним состоянием объекта.

Значение поляactive  изменяется наtrue.

7. Заключение

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

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

Доступен полный исходный кодover on GitHub.