Как тестировать абстрактные классы: дополнить заглушками? - PullRequest
414 голосов
/ 28 октября 2008

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

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

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

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

Ответы [ 14 ]

415 голосов
/ 01 июня 2010

Существует два способа использования абстрактных базовых классов.

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

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


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

Option1

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

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

IMotor

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


Решение для 2

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

AbstractHelper

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

Motor Helper

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


Как правило

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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

244 голосов
/ 28 октября 2008

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

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

11 голосов
/ 28 октября 2008

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

8 голосов
/ 28 октября 2008

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

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

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

8 голосов
/ 28 октября 2008

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

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

6 голосов
/ 04 декабря 2008

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

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

4 голосов
/ 28 октября 2008

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

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);
  }
  /*,,,*/
}

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

4 голосов
/ 28 октября 2008

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

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

3 голосов
/ 28 октября 2008

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

2 голосов
/ 16 мая 2015

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

 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.

       }

   }
...