Чи добре підробляти частину тестуваного класу?


22

Припустимо, у мене є клас (пробачте надуманий приклад та поганий його дизайн):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Зверніть увагу, що методи GetxxxRevenue () та GetxxxExpenses () мають залежності, які витісняються)

Тепер я тестую модулі BothCitiesProfitable (), що залежить від GetNewYorkProfit () та GetMiamiProfit (). Чи добре заглушити GetNewYorkProfit () та GetMiamiProfit ()?

Схоже, якщо я цього не зробив, я одночасно тестую GetNewYorkProfit () та GetMiamiProfit () разом з BothCitiesProfitable (). Мені потрібно переконатися, що я налаштував заглушку для GetxxxRevenue () та GetxxxExpenses (), щоб методи GetxxxProfit () повернули правильні значення.

Поки я бачив лише приклад стримування залежностей від зовнішніх класів, а не внутрішніх методів.

І якщо це нормально, чи є певна модель, яку я повинен використовувати для цього?

ОНОВЛЕННЯ

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

Можливо, мені чогось не вистачає, але я не впевнений, що розділяти клас завжди має сенс. Можливо, ще одним мінімально кращим прикладом буде:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

де повна назва визначається як:

public string FullName()
{
  return FirstName() + " " + LastName();
}

Чи добре заглушувати FirstName () та LastName () під час тестування FullName ()?


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

Щойно дізнавшись про ваше оновлення, я оновив свою відповідь
Вінстон Еверт,

Відповіді:


27

Вам слід розбити розглянутий клас.

Кожен клас повинен виконати кілька простих завдань. Якщо ваше завдання занадто складне для тестування, то завдання, яке виконує клас, занадто велике.

Ігноруючи тугість цієї конструкції:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

ОНОВЛЕННЯ

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

Те, що FullName використовує FirstName та LastName, є детальною частиною реалізації. Нічого поза класом не повинно піклуватися про те, щоб це було правдою. Знущаючись над загальнодоступними методами з метою перевірки об'єкта, ви робите припущення про те, що цей об'єкт реалізований.

В якийсь момент майбутнього це припущення може перестати бути правильним. Можливо, вся логіка імен буде перенесена на об'єкт Name, який Особа просто викликає. Можливо, FullName отримає прямий доступ до змінних учасників first_name та прізвище, а не викликає FirstName та LastName.

Друге питання - чому ви відчуваєте потребу в цьому. Зрештою, ваш клас людини може бути перевірений на кшталт:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

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

Єдиний випадок, коли, мабуть, має сенс знущатися з методів, використовуваних FullName, - якщо якось FirstName () та LastName () були нетривіальними операціями. Можливо, ви пишете один із цих генераторів випадкових імен, або FirstName та LastName запитуєте базу даних для відповіді, чи щось. Але якщо це відбувається, то це дозволяє припустити, що об'єкт робить щось, що не належить до класу Person.

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

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

ОНОВЛЕННЯ ПРОТИ

Як я це бачу, об’єкт має зовнішню і внутрішню поведінку. Зовнішня поведінка включає виклики, що повертають значення в інші об'єкти тощо. Очевидно, що все в цій категорії повинно бути протестовано (інакше що б ви протестували?) Але внутрішню поведінку насправді не слід перевіряти.

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

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

Але яка різниця це має? Якщо FirstName () та LastName () є членами іншого об’єкта, чи дійсно це змінить проблему FullName ()? Якщо ми вирішимо, що потрібно знущатися над FirstName та LastName, чи справді це допомога їм опинитися на іншому об’єкті?

Я думаю, якщо використовувати свій глузливий підхід, то створюєш шов в об’єкті. У вас є такі функції, як FirstName () та LastName (), які безпосередньо спілкуються із зовнішнім джерелом даних. Ви також маєте FullName (), який не робить. Але оскільки всі вони в одному класі, це не видно. Деякі фрагменти не мають прямого доступу до джерела даних, а інші є. Ваш код стане чіткішим, якщо просто розбити ці дві групи.

EDIT

Давайте зробимо крок назад і запитаємо: чому ми знущаємось над об’єктами, коли тестуємо?

  1. Зробіть тести послідовними (уникайте доступу до речей, які змінюються від запуску до запуску)
  2. Уникайте доступу до дорогих ресурсів (не потрапляйте на сторонні послуги тощо)
  3. Спростіть тестувану систему
  4. Спростіть тестування всіх можливих сценаріїв (наприклад, таких як моделювання відмови тощо)
  5. Уникайте залежно від деталей інших фрагментів коду, щоб зміни в цих інших фрагментах коду не порушили цей тест.

Тепер я думаю, що причини 1-4 не стосуються цього сценарію. Знущання над зовнішнім джерелом під час тестування повного імені вирішує всі ці причини глузування. Єдина деталь, яка не обробляється, це простота, але, здається, об’єкт є досить простим, що не викликає особливих проблем.

Я думаю, що ваше занепокоєння є причиною №5. Занепокоєння полягає в тому, що в якийсь момент зміна впровадження FirstName та LastName порушить тест. Надалі FirstName та LastName можуть отримати імена з іншого місця розташування чи джерела. Але FullName, мабуть, завжди буде FirstName() + " " + LastName(). Ось чому ви хочете протестувати FullName, глузуючи з FirstName та LastName.

Тоді у вас є деякий підмножина об'єкта людини, який, швидше за все, зміниться, ніж інші. Решта об’єкта використовує цей підмножина. Цей підмножина наразі отримує свої дані за допомогою одного джерела, але пізніше може отримати ці дані зовсім іншим чином. Але мені здається, що підмножина - це виразний об'єкт, який намагається вийти.

Мені здається, що якщо ви знущаєтесь над методом об’єкта, ви розділяєте об'єкт. Але ви робите це тимчасово. Ваш код не дає зрозуміти, що всередині вашого об'єкта Person є дві чіткі фігури. Тому просто розділіть цей об’єкт у фактичному коді, щоб з читання вашого коду було зрозуміло, що відбувається. Виберіть фактичний розкол об’єкта, який має сенс, і не намагайтеся розділити об'єкт по-різному для кожного тесту.

Я підозрюю, що ви можете заперечити, щоб розколоти ваш об’єкт, але чому?

EDIT

Я був неправий.

Ви повинні розділити об'єкти, а не вводити спеціальні розщеплення, глузуючи з окремих методів. Однак я був надто зосереджений на одному методі розбиття об’єктів. Однак OO забезпечує безліч способів розщеплення об'єкта.

Що я пропоную:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

Можливо, саме цим ви займалися весь час. Але я не думаю, що цей метод не матиме проблем, які я бачив із глузуючими методами, оскільки ми чітко окреслили, на якій стороні кожен метод. І, використовуючи успадкування, ми уникаємо незручностей, які виникли б, якби ми використали додатковий об’єкт обгортки.

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


8
Добре сказано. Якщо ви намагаєтесь знайти обхідні шляхи тестування, вам, ймовірно, потрібно переглянути свій дизайн (як бічна примітка, зараз у мене в голові застряг голос Френка Сінатри).
Проблемний

2
"Знущаючись над загальнодоступними методами з метою перевірки об'єкта, ви робите припущення про [як] цей об'єкт реалізований." Але чи не так це кожного разу, коли ви заглушите предмет? Наприклад, припустимо, я знаю, що мій метод xyz () викликає якийсь інший об'єкт. Щоб перевірити xyz () в ізоляції, я повинен заглушити інший об'єкт. Тому мій тест повинен знати про деталі реалізації мого методу xyz ().
Користувач

У моєму випадку методи "FirstName ()" та "LastName ()" прості, але вони запитують треті сторони api для їх результату.
Користувач

@ Користувач, оновлений, імовірно, ви знущаєтесь із сторонніми api для тестування FirstName та LastName. Що поганого в тому, що робити те ж глузування під час тестування FullName?
Вінстон Еверт

@Winston, це все моє значення, зараз я знущаюся над стороннім api, який використовується в імені та прізвища, щоб перевірити fullname (), але я вважаю за краще не піклуватися про те, як ім’я та прізвище реалізуються при тестуванні fullname (звичайно, нормально, коли я тестую ім'я та прізвище). Звідси моє запитання щодо глузування імені та прізвища.
Користувач

10

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

У такому випадку, замість глузливих методів, я б писав свої тести, щоб поступово висвітлювати клас. Тестуйте getExpenses()і getRevenue()методи, а потім протестуйте getProfit()методи, а потім протестуйте спільні методи. Це правда, що у вас буде більше одного тесту, що охоплює певний метод, але, оскільки ви писали проходження тестів для індивідуального висвітлення методів, ви можете бути впевнені, що ваші результати надійні, отримавши перевірену інформацію.


1
Домовились. Але просто наголосити на тому, хто це читає, це рішення, яке ви використовуєте, якщо не можете зробити краще рішення.
Вінстон Еверт

5
@Winston, і це рішення, яке ви використовуєте, перш ніж дійти до кращого рішення. Тобто у вас є великий куля застарілого коду, ви повинні покрити його одиничними тестами, перш ніж ви зможете його переробити.
Péter Török

@ Péter Török, хороший момент. Хоча я вважаю, що це висвітлено в розділі "кращого рішення не вдається". :)
Вінстон Еверт

@all Тестування методу getProfit () буде дуже складним, оскільки різні сценарії getExpenses () і getRevenues () фактично примножать сценарії, необхідні для getProfit (). якщо ми тестуємо getExpenses () і отримуємо доходи () самостійно. Чи не дуже добре знущатися з цих методів для тестування getProfit ()?
Мохд Фарид

7

Для подальшого спрощення ваших прикладів скажіть, що ви протестуєте C(), що залежить від A()і B()кожен з яких має свої залежності. IMO зводиться до того, чого намагається досягти ваш тест.

Якщо ви випробовуєте поведінку за C()відомими способами поведінки, A()і B()тоді, мабуть, найпростіше і найкраще заглушити A()і B(). Це, мабуть, пуристи називали б одиничним тестом.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.