Які принципи дизайну просувають тестуваний код? (розробка тестового коду проти водіння дизайну за допомогою тестів)


54

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

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

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

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

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


3
Подивіться на це: googletesting.blogspot.in/2008/08/…
VS1

Дякую. Я лише почав читати статтю, і це вже має сенс.

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

Відповіді:


56

Так, SOLID - це дуже хороший спосіб розробити код, який можна легко перевірити. Як короткий буквар:

S - Принцип єдиної відповідальності: Об'єкт повинен робити саме одне і повинен бути єдиним об'єктом у кодовій базі, який робить це одне. Наприклад, візьміть клас домену, скажімо, рахунок-фактура. Клас рахунків-фактур повинен представляти структуру даних та бізнес-правила рахунків-фактур, які використовуються в системі. Це повинен бути єдиний клас, який представляє рахунок-фактуру в кодовій базі. Це може бути додатково розбито, щоб сказати, що метод повинен мати одну мету і бути єдиним методом у кодовій базі, який відповідає цій потребі.

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

O - Відкритий / закритий принцип: Клас повинен бути відкритим для розширення, але закритим для зміни . Коли об'єкт існує і працює правильно, в ідеалі не повинно виникати необхідності повертатися до нього, щоб вносити зміни, що додають нову функціональність. Натомість об’єкт повинен бути розширений або шляхом його отримання, або за допомогою підключення до нього нових або різних реалізацій залежностей, щоб забезпечити цю нову функціональність. Це дозволяє уникнути регресу; ви можете ввести новий функціонал, коли і де це потрібно, не змінюючи поведінку об'єкта, як це вже використовується в іншому місці.

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

L - Принцип заміщення Ліскова: Клас А, залежний від класу В, повинен мати можливість використовувати будь-який X: B, не знаючи різниці. Це в основному означає, що все, що ви використовуєте як залежність, повинно мати подібну поведінку, як це бачить залежний клас. Як короткий приклад, скажімо, що у вас є інтерфейс IWriter, який відкриває Write (рядок), який реалізується ConsoleWriter. Тепер вам потрібно записати у файл, щоб ви створили FileWriter. Роблячи це, ви повинні переконатися, що FileWriter можна використовувати так само, як це робив ConsoleWriter (маючи на увазі, що єдиний спосіб залежних може взаємодіяти з ним, зателефонувавши Write (string)), і так додаткову інформацію, що FileWriter може знадобитися для цього робота (наприклад, шлях та файл для запису) має бути надана з іншого місця, ніж залежного.

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

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

Прихильність до ISP покращує доказовість, зменшуючи складність систем, що перевіряються, та залежності цих SUT. Якщо об'єкт, який ви тестуєте, залежить від інтерфейсу IDoThreeThings, який відкриває DoOne (), DoTwo () і DoThree (), ви повинні знущатися над об'єктом, який реалізує всі три методи, навіть якщо для об'єкта використовується тільки метод DoTwo. Але, якщо об’єкт залежить лише від IDoTwo (який виставляє лише DoTwo), ви можете легше знущатися над об’єктом, у якого є один метод.

D - Принцип інверсії залежності: Конкреції та абстракції ніколи не повинні залежати від інших конкрементів, а від абстракцій . Цей принцип безпосередньо виконує принцип нещільного з’єднання. Об'єкт ніколи не повинен знати, що таке об’єкт; натомість слід дбати про те, що робить об’єкт. Отже, використання інтерфейсів та / або абстрактних базових класів завжди слід віддати перевагу використанню конкретних реалізацій при визначенні властивостей та параметрів об’єкта чи методу. Це дозволяє обміняти одну реалізацію на іншу, не змінюючи використання (якщо ви також слідуєте за LSP, що йде рука об руку з DIP).

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


16

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

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

З мого досвіду, 2 принципи SOLID відіграють головну роль у розробці тестового коду:

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

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

(...) чи добре перефактурувати існуючий продукт / проект та вносити зміни до коду та дизайну, щоб мати можливість написати одиничний тестовий випадок для кожного модуля?

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

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

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


iaw висока згуртованість та низька муфта
jk.

8

ВАШЕ ПЕРШЕ ЗАПИТАННЯ:

SOLID - це справді шлях. Я вважаю, що два найважливіші аспекти акроніму SOLID, якщо мова йде про перевірку, - це S (Одинична відповідальність) та D (Введення залежностей).

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

Вприскування в залежності (DI): Це дає вам змогу контролювати середовище тестування. Замість створення об’єктів forreign всередині вашого коду ви вводите його через конструктор класів або виклик методу. Під час тестування одиниць ви просто замінюєте реальні класи на заглушки або макети, якими ви керуєте повністю.

ВАШЕ ДРУГЕ ПИТАННЯ: В ідеалі ви пишете тести, які документують функціонування вашого коду, перш ніж його рефакторинг. Таким чином, ви можете задокументувати, що ваш рефакторинг відтворює ті самі результати, що й вихідний код. Однак ваша проблема полягає в тому, що функціонуючий код важко перевірити. Це класична ситуація! Моя порада: Подумайте про рефакторинг перед тестуванням одиниць. Якщо можеш; написати тести для робочого коду, потім перефактурувати код, а потім повторно змінити тести. Я знаю, що це коштуватиме годин, але ви будете впевненіші, що код, що ремонтується, виконує те саме, що і старий. Сказавши це, я багато разів здавався. Заняття можуть бути настільки потворними та безладними, що переписання - єдиний спосіб зробити їх перевірятими.


4

На додаток до інших відповідей, які зосереджуються на досягненні вільної зв'язку, я хотів би сказати слово про тестування складної логіки.

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

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

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

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


3

SOLID - це чудовий початок, на мій досвід, чотири аспекти SOLID дійсно добре справляються з тестуванням одиниць.

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

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

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

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

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


1
Це називається принципом "інверсії залежності", а не принципом "ін'єкції".
Mathias Lykkegaard Lorenzen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.