Одиночні чи декілька файлів для тестування одиничного класу?


20

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

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

Приклад сценарію - клас (називайте його Document), який має два методи: CheckIn і CheckOut. Кожен метод реалізує різні правила тощо, які контролюють їх поведінку. Дотримуючись правила «одне твердження за тест», у мене буде кілька тестів для кожного методу. Я можу розмістити всі тести в одному DocumentTestsкласі з іменами типу " CheckInShouldThrowExceptionWhenUserIsUnauthorizedі" CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Або я міг би мати два окремі тестові класи: CheckInShouldі CheckOutShould. У цьому випадку мої тестові імена будуть скорочені, але вони будуть організовані, щоб усі тести на конкретну поведінку (метод) були разом.

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


2
Назви методів тестування занадто довгі. Спростіть їх, навіть якщо це означає, що метод випробування містив би кілька тверджень.
Бернар

12
@ Бернарда: вважається поганою практикою мати кілька тверджень на метод, проте довгі назви методів тестування НЕ вважаються поганою практикою. Зазвичай ми документуємо те, що метод тестує в самій назві. Наприклад, constructor_nullSomeList (), setUserName_UserNameIsNotValid () тощо ...
c_maker

4
@ Бернар: Я також використовую довгі тестові імена (і тільки там!). Я люблю їх, тому що так зрозуміло, що не працювало (якщо ваші імена - хороший вибір;)).
Себастьян Бауер

Кілька тверджень не є поганою практикою, якщо всі вони перевіряють однаковий результат. Напр. ви не мали б testResponseContainsSuccessTrue(), testResponseContainsMyData()і testResponseStatusCodeIsOk(). Ви б їх в один , testResponse()який три стверджує: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)іassertEquals(true, response.success)
Юха Untinen

Відповіді:


18

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


3
Хороший приклад, чому саме цей підхід був представлений мені в першу чергу. Наприклад, коли я хочу протестувати метод CheckIn, я завжди хочу, щоб тестований об'єкт був встановлений в одну сторону, але коли я тестую метод CheckOut, мені потрібні інші настройки. Гарна думка.
SonOfPirate

14

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

  1. Не потрібно дублювати (і підтримувати кілька версій) коду настройки
  2. Простіше запустити всі тести для класу з IDE, якщо вони згруповані в одному тестовому класі
  3. Легше вирішити неполадки за допомогою однозначного відображення між тестовими класами та класами.

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

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

Наприклад, у JUnit ви можете перевірити речі за допомогою різних Runners, що призводить до необхідності різних класів.
Йоахім Нілссон

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

7

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

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


На жаль, у реальному світі не всі класи дотримуються SRP та ін ..., і це стає проблемою, коли ви перевіряєте застарілий код.
Péter Török

3
Не впевнений, як тут відноситься SRP. У моєму класі є кілька методів, і мені потрібно писати одиничні тести на всі різні вимоги, що визначають їх поведінку. Чи краще мати один тестовий клас з 50 методами тестування або 5 тестових класів з 10 тестами, кожен з яких стосується конкретного методу в досліджуваному класі?
SonOfPirate

2
@SonOfPirate: Якщо вам потрібно стільки тестів, це може означати, що ваші методи роблять занадто багато. Тому SRP тут дуже важливий. Якщо ви застосовуєте SRP як до класів, так і до методів, у вас буде безліч малих класів і методів, які легко перевірити, і вам не буде потрібно стільки методів тестування. (Але я погоджуюся з Петером Тьореком, що коли ви перевіряєте застарілий код, передовий досвід виходить за двері, і ви просто повинні зробити все можливе, що вам дано ...)
c_maker

Найкращий приклад, який я можу навести, - це метод CheckIn, у якому є ділові правила, які визначають, кому дозволено виконувати дію (авторизацію), якщо об’єкт перебуває у належному стані, який слід перевіряти, наприклад, якщо він перевірений і якщо зміни були зроблені. У нас будуть одиничні тести, які підтверджують, що правила авторизації виконуються, а також тести на те, щоб метод поводився правильно, якщо CheckIn викликається, коли об'єкт не перевірено, не змінено і т. Д. Тому ми закінчуємо багато тести. Ви припускаєте, що це неправильно?
SonOfPirate

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

2

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

У випадку, коли різні тести вимагають різко встановлених налаштувань, я бачив, як деякі практикуючі TDD ставлять «кріплення» або «налаштування» в різні класи / файли, але не самі тести.


1

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

Ось приклад застосування цього підходу до мого оригінального сценарію:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Це призводить до появи таких тестів у списку тестів:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

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

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


1

Постарайтеся на хвилину подумати навпаки.

Чому б у вас був лише один тестовий клас на клас? Ви робите тест класу чи тест на одиницю? Ви навіть залежите від занять ?

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

Як ви думаєте про цей псевдо-код:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Тепер ви безпосередньо не тестуєте checkInметод. Ви замість цього тестуєте поведінку (що означає перевірити на щастя;)), і якщо вам потрібно переробити Documentі розділити її на різні класи, або об'єднати в неї інший клас, ваші тестові файли все ще є когерентними, вони все одно мають сенс, оскільки ви ніколи не змінюєте логіку при рефакторингу, лише структуру.

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


0

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

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


0

1. Поміркуйте, що може піти не так

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

Це стає більш цікавим, коли тест виходить з ладу після зміни набагато пізніше. Є дві можливості:

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

2. Поміркуйте, для чого використовуються тести

Тести служать двом основним цілям:

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

Так?

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

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