Как тестировать абстрактные классы: дополнить заглушками?

446

Мне было интересно, как тестировать абстрактные классы и классы, расширяющие абстрактные классы.

Должен ли я тестировать абстрактный класс, расширяя его, заглушая абстрактные методы, а затем тестируя все конкретные методы? Тогда только тестируйте методы, которые я переопределяю, и тестируйте абстрактные методы в модульных тестах для объектов, расширяющих мой абстрактный класс?

Должен ли я иметь абстрактный контрольный пример, который можно использовать для проверки методов абстрактного класса, и расширить этот класс в моем тестовом примере для объектов, расширяющих абстрактный класс?

Обратите внимание, что мой абстрактный класс имеет несколько конкретных методов.

Пол Уилан
источник

Ответы:

268

Напишите объект Mock и используйте его только для тестирования. Обычно они очень, очень, очень минимальны (наследуются от абстрактного класса) и не более. Затем в модульном тесте вы можете вызвать абстрактный метод, который вы хотите протестировать.

Вы должны протестировать абстрактный класс, который содержит некоторую логику, как и все остальные классы, которые у вас есть.

Патрик Дежарден
источник
9
Черт, я должен сказать, что я впервые согласился с идеей использовать макет.
Джонатан Аллен
5
Вам нужно два класса, макет и тест. Класс mock расширяет только абстрактные методы тестируемого класса Abstract. Этими методами могут быть no-op, return null и т. Д., Поскольку они не будут проверяться. Тестовый класс проверяет только неабстрактный публичный API (т. Е. Интерфейс, реализованный классом Abstract). Для любого класса, который расширяет класс Abstract, вам понадобятся дополнительные тестовые классы, потому что абстрактные методы не были охвачены.
кибер-монах
10
Очевидно, что это можно сделать ... но чтобы действительно протестировать любой производный класс, вы собираетесь снова и снова тестировать эту базовую функциональность ... что приводит к тому, что у вас будет абстрактное тестовое устройство, чтобы вы могли исключить это дублирование при тестировании. Это все пахнет! Я настоятельно рекомендую еще раз взглянуть на то, почему вы используете абстрактные классы, и посмотреть, будет ли что-то еще работать лучше.
Найджел Торн
5
Перефразированный следующий ответ намного лучше.
Мартин Спамер
22
@MartiSpamer: Я бы не сказал, что этот ответ переоценен, потому что он был написан намного раньше (на 2 года), чем ответ, который вы считаете ниже. Давайте просто подбадриваем Патрика, потому что в контексте того, когда он опубликовал этот ответ, это было здорово. Давайте поддерживать друг друга. Приветствия
Марвин Тобеджэйн
449

Есть два способа использования абстрактных базовых классов.

  1. Вы специализируете свой абстрактный объект, но все клиенты будут использовать производный класс через его базовый интерфейс.

  2. Вы используете абстрактный базовый класс, чтобы исключить дублирование объектов в вашем проекте, а клиенты используют конкретные реализации через свои собственные интерфейсы.


Решение для 1 - шаблон стратегии

Опция 1

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

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

IMotor

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


Решение для 2

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

AbstractHelper

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

Мотор Хелпер

Это снова приводит к конкретным классам, которые просты и легко тестируемы.


Как правило

Пользуйтесь сложной сетью простых объектов над простой сетью сложных объектов.

Ключ к расширяемому тестируемому коду - небольшие строительные блоки и независимая проводка.


Обновлено: как обрабатывать смеси обоих?

Можно иметь базовый класс, выполняющий обе эти роли ... т.е. он имеет открытый интерфейс и имеет защищенные вспомогательные методы. Если это так, то вы можете выделить вспомогательные методы в один класс (script2) и преобразовать дерево наследования в шаблон стратегии.

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


Обновление 2: Абстрактные классы как трамплин (2014/06/12)

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

У нас есть стандартный формат для наших файлов конфигурации. Этот конкретный инструмент имеет 3 файла конфигурации в этом формате. Я хотел иметь строго типизированный класс для каждого файла настроек, чтобы через внедрение зависимостей класс мог запрашивать настройки, о которых он заботился.

Я реализовал это, имея абстрактный базовый класс, который знает, как анализировать форматы файлов настроек и производные классы, которые предоставляют те же методы, но инкапсулируют местоположение файла настроек.

Я мог бы написать «SettingsFileParser», который обернул 3 класса, а затем делегировать его базовому классу для предоставления методов доступа к данным. Я решил не делать этого пока , как это привело бы к 3 производных классов с более делегирования кода в них , чем все остальное.

Однако ... как этот код развивается, и потребители каждого из этих классов настроек становятся понятнее. Каждую настройку пользователи будут запрашивать некоторые настройки и каким-то образом преобразовывать их (поскольку настройки являются текстовыми, они могут обернуть их в объекты, преобразовать их в числа и т. Д.). Когда это произойдет, я начну извлекать эту логику в методы манипулирования данными и возвращать их обратно в строго типизированные классы настроек. Это приведет к более высокоуровневому интерфейсу для каждого набора настроек, который в конечном итоге перестанет осознавать, что имеет дело с «настройками».

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

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

Таким образом, класс Abstract: для меня способ избежать кода делегирования на данный момент и маркер в коде, чтобы напомнить мне о необходимости изменить дизайн позже. Я, возможно, никогда не доберусь до этого, поэтому он может жить долго ... только код может сказать.

Я считаю, что это верно для любого правила ... типа "нет статических методов" или "нет частных методов". Они указывают на запах в коде ... и это хорошо. Это заставляет вас искать абстракцию, которую вы пропустили ... и позволяет в то же время обеспечивать ценность для вашего клиента.

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

Найджел Торн
источник
18
Это отличный ответ. Гораздо лучше, чем с самым высоким рейтингом. Но тогда я думаю, что только те, кто действительно хочет писать тестируемый код, оценят это по достоинству .. :)
MalcomTucker
22
Я не могу понять, насколько хороший ответ на самом деле. Это полностью изменило мой взгляд на абстрактные классы. Спасибо Найджел.
MalcomTucker
4
О нет .. еще один принцип, который я должен переосмыслить! Спасибо (как с сарказмом на данный момент, так и без сарказма за то, что я усвоил это и чувствую себя лучшим программистом)
Martin Lyne
11
Хороший ответ. Определенно, о чем подумать ... но разве не то, что вы говорите, сводится к тому, чтобы не использовать абстрактные классы?
Брианестей
32
+1 за одно правило: «Пользуйтесь сложной сетью простых объектов над простой сетью сложных объектов».
Дэвид Гласс
12

Что я делаю для абстрактных классов и интерфейсов, так это следующее: я пишу тест, который использует объект как конкретный. Но переменная типа X (X является абстрактным классом) не установлена ​​в тесте. Этот тестовый класс не добавляется в набор тестов, но в его подклассы, которые имеют метод установки, устанавливающий переменную для конкретной реализации X. Таким образом, я не дублирую тестовый код. Подклассы неиспользуемого теста могут добавить дополнительные тестовые методы, если это необходимо.

Mnementh
источник
это не вызывает проблемы приведения в подклассе? если X имеет метод a и Y наследует X, но также имеет метод b. Когда вы создаете подкласс своего тестового класса, вам не нужно приводить абстрактную переменную к Y, чтобы выполнять тесты на b?
Джонно Нолан,
8

Чтобы выполнить модульный тест специально для абстрактного класса, вы должны получить его для целей тестирования, тестирования base.method () и предполагаемого поведения при наследовании.

Вы тестируете метод, вызывая его, поэтому тестируйте абстрактный класс, реализуя его ...


источник
8

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

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

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

Сет Петри-Джонсон
источник
6

Один из способов - написать абстрактный контрольный пример, соответствующий вашему абстрактному классу, а затем написать конкретные контрольные примеры, которые подклассируют ваш абстрактный контрольный пример. сделайте это для каждого конкретного подкласса вашего исходного абстрактного класса (т.е. ваша иерархия тестовых примеров отражает вашу иерархию классов). см. Проверка интерфейса в книге получателей junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

также см. Суперкласс Testcase в шаблонах xUnit: http://xunitpatterns.com/Testcase%20Superclass.html

Рэй Тайек
источник
4

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

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

casademora
источник
Я не хотел повторно тестировать код, который я уже тестировал, поэтому я шел по пути абстрактного теста. Я пытаюсь проверить все конкретные методы в моем абстрактном классе в одном месте.
Пол Уилан
7
Я не согласен с извлечением общих элементов для вспомогательных классов, по крайней мере, в некоторых (многих?) Случаях. Если абстрактный класс содержит какую-то конкретную функциональность, я думаю, что вполне приемлемо тестировать эту функциональность напрямую.
Сет Петри-Джонсон
4

Это шаблон, который я обычно использую при настройке привязки для тестирования абстрактного класса:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

И версия, которую я использую в тесте:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

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


источник
3

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

Джеб
источник
2

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

туз
источник
2

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

С точки зрения модульного тестирования необходимо учитывать две вещи:

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

  2. Функциональность производных классов . Если у вас есть пользовательская логика, которую вы написали для своих производных классов, вам следует тестировать эти классы изолированно.

редактировать: RhinoMocks - это отличная среда для фиктивного тестирования, которая может генерировать фиктивные объекты во время выполнения, динамически наследуя ваш класс. Такой подход может сэкономить вам бесчисленные часы ручного кодирования производных классов.

bryanbcook
источник
2

Во-первых, если абстрактный класс содержит какой-то конкретный метод, я думаю, вы должны сделать это, рассмотрев этот пример

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }
Shreeram Banne
источник
1

Если абстрактный класс подходит для вашей реализации, протестируйте (как предложено выше) производный конкретный класс. Ваши предположения верны.

Чтобы избежать путаницы в будущем, имейте в виду, что этот конкретный тестовый класс - не подделка , а подделка .

В строгом смысле, макет определяется следующими характеристиками:

  • Макет используется вместо каждой зависимости испытуемого класса.
  • Макет - это псевдо-реализация интерфейса (вы можете вспомнить, что, как правило, зависимости должны быть объявлены как интерфейсы; тестируемость является одной из основных причин этого)
  • Поведение членов интерфейса макета - будь то методы или свойства - предоставляется во время тестирования (опять же, с использованием фальшивой среды). Таким образом вы избегаете связи тестируемой реализации с реализацией ее зависимостей (которые должны иметь свои собственные дискретные тесты).
banduki
источник
1

После ответа @ patrick-desjardins я реализовал абстрактный и его класс реализации, @Testа также:

Абстрактный класс - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Поскольку абстрактные классы не могут быть созданы, но могут быть разделены на подклассы , конкретный класс DEF.java выглядит следующим образом:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

Класс @Test для тестирования как абстрактного, так и неабстрактного метода:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
Арпит Аггарвал
источник