Чи варто жорстко кодувати свої дані у всіх тестах одиниць?


33

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

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

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

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

Оновлення

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

Мій поточний проект - це ASP.NET MVC, який спочатку використовує блок роботи над Entity Framework Code та Moq для тестування. Я знущався над UoW та сховищами, але використовую реальні класи бізнес-логіки та тестую дії контролера. Тести часто перевіряють, чи було виконано UOW, наприклад:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBaseбудує макет UoW та інстанціює userLogic.

Багато тестів вимагає наявності в базі даних вже наявного користувача або продукту, тому я попередньо заповнив те, що повертається знущається UoW, у цьому прикладі userData, який є лише IList<User>одним записом користувача.


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

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

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

Книга "xUnit Test Patterns" дає вагомий привід для пристосувань для багаторазового використання та помічників. Тестовий код повинен бути таким же ретельним, як і будь-який інший код.
Чак Крутсінгер

Ця стаття може бути корисною: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Відповіді:


25

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

Я використовую підхід мати стандартні класи TestHelper, які забезпечують мені безліч типів даних, якими я регулярно користуюсь, тому я можу створювати набори стандартних класів сутності або DTO для моїх тестів, щоб запитувати і точно знати, що я отримуватиму щоразу. Тому я можу зателефонувати, TestHelper.GetFooRange( 0, 100 )щоб отримати діапазон 100 об'єктів Foo з усіма їх залежними класами / полями.

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

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

Хоча в одиничних тестах слід бути обережними:

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

10

Що б не зробило наміри вашого тесту більш читабельним.

Як загальне правило:

Якщо дані є частиною тесту (наприклад, не слід друкувати рядки зі станом 7), кодуйте їх у тесті, щоб було зрозуміло, що автор мав намір статися.

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

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


+1 Я згоден Це пахне тим, що він тестує - це щільно з'єднати для одиничного тестування.
Реакційний

5

Різні методи тестування

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

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

Умови, якими ми користуємося, трохи залежать від платформи, але їх можна знайти майже на всіх тестових / розробних платформах:

Приклад застосування

Залежно від технології, якою ви користуєтесь, імена можуть відрізнятися, але я буду використовувати це як приклад:

Якщо у вас є проста програма CRUD із моделлю Product, ProductsController та індексним представленням, яка генерує таблицю HTML з продуктами:

Кінцевим результатом програми є показ таблиці HTML із переліком усіх активних продуктів.

Блок тестування

Модель

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

Потім ми запускаємо наш тест, наприклад: testGetAllActive products.

Тож ми перевіряємо безпосередньо тестову базу даних. Ми не знущаємось над джерелом даних; ми робимо це завжди однаково. Це дозволяє нам, наприклад, протестувати нову версію бази даних, і виникнуть будь-які проблеми із запитами.

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

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

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

Контролер

Контролеру потрібно більше роботи, тому що ми не хочемо тестувати модель з ним. Отже, ми робимо знущання над моделлю. Це означає: Ми перевіряємо: index () метод, який повинен повернути список записів.

Тож ми знущаємося з модельного методу getAllActive () і додаємо до нього фіксовані дані (наприклад, два записи). Тепер ми перевіряємо дані, які контролер надсилає до перегляду, і порівнюємо, чи справді ми повернемо ці два записи назад.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

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

Отже, контролеру потрібна нормальна макет і невеликий фрагмент даних з твердим кодом. Для системи входу можливо ще одна. У нашому тесті у нас є хелперний метод для цього: setLoggedIn (). Це спрощує тестування з логіном або без входу.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Перегляди

Тестування переглядів важко. Спочатку ми відокремлюємо логіку, яка повторюється. Ми поміщаємо це в Helpers і перевіряємо ці класи суворо. Ми очікуємо завжди однакового результату. Наприклад, createHtmlTableFromArray ().

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

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

echo $this->tableHelper->generateHtmlTableFromArray($products);

Інтеграційне тестування

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

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

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

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

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}

Це чудова відповідь, на зовсім інше питання.
пдр

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

Відповідь було оновлено кількома прикладами коду, щоб пояснити, як перевірити, не викликаючи всілякі інші класи.
Люк Франкен

4

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

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

З іншого боку, для модульних тестів я рекомендував би тримати ціль тестового класу єдиним "реальним" класом і знущатися над усім іншим. Тоді вам слід дуже жорстко кодувати дані тесту, щоб переконатися, що ви покрили якомога більше спеціальних / попередніх помилок.

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


-1

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

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

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

Однак, я думаю, що у ваших тестах є також користь мати деякі випадкові дані.


Ви справді читали питання, а не лише заголовок?
Якоб

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

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