Як з’єднати тестові абстрактні класи: продовжити за допомогою заглушок?


446

Мені було цікаво, як розділити тестові абстрактні класи та класи, що розширюють абстрактні класи.

Чи слід перевірити абстрактний клас, розширивши його, заглушивши абстрактні методи, а потім перевірити всі конкретні методи? Тоді тестуйте лише методи, які я перекриваю, і тестую абстрактні методи в одиничних тестах для об'єктів, що розширюють мій абстрактний клас?

Чи повинен у мене бути абстрактний тестовий випадок, який можна використовувати для тестування методів абстрактного класу та розширити цей клас у моєму тестовому випадку для об’єктів, що розширюють абстрактний клас?

Зауважте, що мій абстрактний клас має деякі конкретні методи.

Відповіді:


268

Напишіть об'єкт Mock і використовуйте їх лише для тестування. Зазвичай вони дуже дуже мінімальні (успадковуються від абстрактного класу) і не більше. Тоді у вашому Unit Test ви можете викликати абстрактний метод, який ви хочете перевірити.

Ви повинні перевірити абстрактний клас, який містить певну логіку, як і всі інші класи, які у вас є.


9
Чорт, я мушу сказати, що це перший раз, коли я коли-небудь погоджувався з ідеєю використання макету.
Джонатан Аллен

5
Вам потрібні два заняття, макет і тест. Модельний клас поширюється лише на абстрактні методи тестуваного класу «Абстрактні». Ці методи можуть бути неоперативними, повертати нульовими і т. Д., Оскільки вони не будуть перевірені. Тестовий клас випробовує лише неабразований загальнодоступний API (тобто інтерфейс, реалізований класом "Анотація"). Для будь-якого класу, що розширює клас Анотація, вам знадобляться додаткові тестові класи, оскільки абстрактні методи не були охоплені.
кібермонах

10
Очевидно, це можна зробити .. але по-справжньому протестувати будь-який похідний клас, ви будете тестувати цю базову функціональність знову і знову .., що призводить до того, що у вас є абстрактний прилад для тестування, щоб ви могли визначити це дублювання при тестуванні. Це все пахне! Я настійно рекомендую ще раз поглянути на те, чому ви в першу чергу використовуєте абстрактні заняття, і подивіться, чи буде щось краще працювати.
Найджел Торн

5
Наступна відповідь за надмірною оцінкою набагато краща.
Мартін Спамер

22
@MartiSpamer: Я б не сказав, що ця відповідь завищена, оскільки вона була написана набагато раніше (2 роки), ніж відповідь, яку ви вважаєте кращою нижче. Давайте просто заохочувати Патріка, оскільки в контексті, коли він опублікував цю відповідь, це було чудово. Давайте заохочувати одне одного. Ура
Марвін Тхобеджане

449

Є два способи використання абстрактних базових класів.

  1. Ви спеціалізуєтеся на своєму абстрактному об'єкті, але всі клієнти будуть використовувати похідний клас через його базовий інтерфейс.

  2. Ви використовуєте абстрактний базовий клас, щоб виділити дублювання в об'єктах у вашому дизайні, а клієнти використовують конкретні реалізації через власні інтерфейси!


Рішення для 1 - стратегія

Варіант1

Якщо у вас є перша ситуація, ви фактично маєте інтерфейс, визначений віртуальними методами в абстрактному класі, який реалізують ваші похідні класи.

Вам слід розглянути можливість створення справжнього інтерфейсу, змінити абстрактний клас на конкретний, і взяти примірник цього інтерфейсу в його конструкторі. Потім отримані класи стануть реалізацією цього нового інтерфейсу.

Імотор

Це означає, що тепер ви можете протестувати свій раніше абстрактний клас, використовуючи макетний екземпляр нового інтерфейсу та кожну нову реалізацію через теперішній загальнодоступний інтерфейс. Все просто і перевіряється.


Рішення для 2

Якщо у вас є друга ситуація, то ваш абстрактний клас працює як помічник.

AbstractHelper

Погляньте на функціонал, який він містить. Подивіться, чи можна будь-який із них натиснути на об'єкти, якими маніпулюють, щоб мінімізувати це дублювання. Якщо у вас все ще залишається, погляньте на те, щоб зробити його допоміжним класом, який ваша конкретна реалізація бере у своєму конструкторі, і видаліть їх базовий клас.

Мотор-помічник

Це знову ж таки призводить до конкретних занять, які прості та легко перевіряються.


Як правило

Віддавайте перевагу складній мережі простих об'єктів над простою мережею складних об'єктів.

Ключ до розширюваного тестового коду - це невеликі будівельні блоки та незалежна електропроводка.


Оновлено: Як обробляти суміші обох?

Можна мати базовий клас, який виконує обидві ці ролі ... тобто: він має загальнодоступний інтерфейс і захищені допоміжні методи. Якщо це так, то ви можете розподілити допоміжні методи в один клас (сценарій2) і перетворити дерево спадкування в шаблон стратегії.

Якщо ви виявите, що у вас є деякі методи, які ваш базовий клас реалізує безпосередньо, а інші - віртуальні, тоді ви все одно можете перетворити дерево спадкування в шаблон стратегії, але я також вважаю це хорошим показником того, що обов'язки не вирівняні правильно, і, можливо, потребують рефакторингу.


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

Днями у мене була ситуація, коли я використовував конспект, тому хотів би вивчити чому.

Для наших конфігураційних файлів у нас є стандартний формат. Цей конкретний інструмент має 3 файли конфігурації, всі у цьому форматі. Я хотів, щоб сильно набрали клас для кожного файла налаштувань, щоб через введення залежності клас міг попросити налаштування, про які він піклувався.

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

Я міг би написати "SettingsFileParser", який три класи обернули, а потім делегували до базового класу, щоб відкрити методи доступу до даних. Я вирішив поки що цього не робити, оскільки це призведе до 3 похідних класів з більшою кількістю коду делегації, ніж будь-що інше.

Однак ... у міру того, як цей код розвивається і споживачі кожного з цих класів налаштувань стають зрозумілішими. Кожен користувач налаштувань запитає деякі параметри та певним чином перетворить їх (оскільки налаштування є текстовими, вони можуть обернути їх в об'єкти перетворення їх у числа тощо). Як це станеться, я почну видобувати цю логіку в методах обробки даних і відштовхувати їх на сильно набрані класи налаштувань. Це призведе до вищого рівня інтерфейсу для кожного набору налаштувань, який з часом вже не знає, що він має справу з "налаштуваннями".

На даний момент сильно набрані класи налаштувань більше не потребуватимуть методів "getter", які розкривають основні "налаштування".

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

Отже, клас "Анотація" - це спосіб мені уникнути делегування коду на даний момент та маркер у коді, який нагадує мені пізніше змінити дизайн. Я ніколи не можу дістатись до нього, тому він може прожити добрий час ... тільки код може розказати.

Я вважаю це правдою з будь-яким правилом ... як "немає статичних методів" або "немає приватних методів". Вони вказують запах у коді ... і це добре. Це змушує вас шукати абстракцію, яку ви пропустили ... і дозволяє продовжувати надавати цінність для свого клієнта в середній час.

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


18
Це чудова відповідь. Набагато краще, ніж найвищий рейтинг. Але тоді я думаю, що оцінювали б лише ті, хто дуже хоче написати тестовий код .. :)
MalcomTucker

22
Я не можу зрозуміти, наскільки це справді хороша відповідь. Це повністю змінило мій погляд на абстрактні заняття. Спасибі Найджел.
MalcomTucker

4
О ні .. ще один принцип, який я повинен гон переосмислити! Дякую (як саркастично зараз, так і не саркастично за те, що я засвоїв це і відчуваю себе кращим програмістом)
Мартін Лін

11
Гарна відповідь. Безумовно, над чим подумати ... але чи не те, що ви говорите, в основному зводиться до того, щоб не використовувати абстрактні класи?
brianestey

32
+1 лише для Правила: "Віддайте перевагу складній мережі простих об'єктів над простою мережею складних об'єктів".
Девід Гласс

12

Що я роблю для абстрактних класів та інтерфейсів, це таке: я пишу тест, який використовує об'єкт як конкретний. Але змінна типу X (X - абстрактний клас) не встановлена ​​в тесті. Цей тестовий клас не додається до набору тестів, а його підкласи, які мають метод настройки, який встановлює змінну до конкретної реалізації X. Таким чином я не дублюю тестовий код. Підкласи не використовуваного тесту можуть додавати більше методів тестування, якщо це необхідно.


це не викликає проблем з литтям у підкласі? якщо X має метод a і Y успадковує X, але має також метод b. Коли ви підкласируєте свій тестовий клас, чи не потрібно приводити свою абстрактну змінну до Y для виконання тестів на b?
Джонно Нолан

8

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

Ви випробовуєте метод, називаючи його таким чином, протестуйте абстрактний клас, реалізуючи його ...


8

Якщо ваш абстрактний клас містить конкретну функціональність, яка має цінність для бізнесу, я, як правило, перевіряю його безпосередньо, створюючи тестовий подвійний, який заглушує абстрактні дані, або використовуючи глузливі рамки, щоб зробити це для мене. Яку з них я вибираю, багато в чому залежить від того, потрібно мені написати тестові реалізації абстрактних методів чи ні.

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

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


6

один із способів - написати абстрактний тестовий випадок, який відповідає вашому абстрактному класу, а потім написати конкретні тестові випадки, які підкласують ваш абстрактний тестовий випадок. зробіть це для кожного конкретного підкласу вашого оригінального абстрактного класу (тобто ієрархія вашого тестового випадку відображає ієрархію вашого класу). див. Тест інтерфейсу в книзі реквізитів за день: http://safari.informit.com/9781932394238/ch02lev1sec6 .

дивіться також тестовий суперклас у шаблонах xUnit: http://xunitpatterns.com/Testcase%20Superclass.html


4

Я б заперечував проти "абстрактних" тестів. Я думаю, що тест є конкретною ідеєю і не має абстракції. Якщо у вас є спільні елементи, введіть їх у допоміжні методи або класи, якими користуватимуться всі.

Що стосується тестування абстрактного тестового класу, переконайтеся, що ви запитаєте себе, що це тестування. Є кілька підходів, і ви повинні з’ясувати, що працює у вашому сценарії. Ви намагаєтеся перевірити новий метод у своєму підкласі? Тоді ваші тести мають взаємодію лише з цим методом. Ви протестуєте методи в базовому класі? Тоді, мабуть, є окреме кріплення лише для цього класу, і випробуйте кожен метод окремо з необхідною кількістю тестів.


Я не хотів повторно перевіряти код. Я вже перевіряв ось чому я йшов по дорозі абстрактного тесту. Я намагаюся перевірити всі конкретні методи в моєму абстрактному класі в одному місці.
Пол Уілан

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 - це дивовижна структура макетного тестування, яка може генерувати макетні об’єкти під час виконання, динамічно виходячи з вашого класу. Такий підхід може заощадити вам незліченну кількість годин з ручним кодуванням похідних класів.


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.

       }

   }

1

Якщо абстрактний клас підходить для вашої реалізації, протестуйте (як було запропоновано вище) похідний конкретний клас. Ваші припущення правильні.

Щоб уникнути майбутньої плутанини, пам’ятайте, що цей конкретний тестовий клас - це не глузування, а підробка .

Суворо кажучи, макет визначається такими характеристиками:

  • Макет використовується замість кожної залежності тестуваного класу.
  • Макет - це псевдореалізація інтерфейсу (ви можете згадати, що, як правило, залежності слід оголошувати інтерфейсами; доказовість - одна з головних причин цього)
  • Поведінка членів інтерфейсу макету - будь то методи чи властивості - надається в тестовий час (знову ж таки, за допомогою знущаючої рамки). Таким чином, ви уникаєте зв’язку тестування реалізації з реалізацією її залежностей (які повинні мати власні дискретні тести).

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!";
    }
}

@ Тест класу для тестування як абстрактних, так і нео абстрактних методів:

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"));
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.