Найкраща практика: ініціалізуйте поля класу JUnit в setUp () або під час декларації?


120

Чи слід ініціалізувати поля класу при такому оголошенні?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Або в setUp (), як це?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

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

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


2
Пам’ятайте, що відповідь відрізняється в JUnit 4 (ініціалізація в декларації) та JUnit 3 (використання setUp); це корінь плутанини.
Нілс фон Барт

Відповіді:


99

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

Коли приклади JUnit створюють ArrayList у методі setUp, всі вони продовжують перевіряти поведінку цього ArrayList з такими випадками, як testIndexOutOfBoundException, testEmptyCollection тощо. Перспектива, коли хтось пише клас і переконається, що він працює правильно.

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

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

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


45

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

Ніщо з цього не стосується прикладу створення порожньої колекції, оскільки це ніколи не викине, але це перевага setUp()методу.


18

Окрім відповіді Алекса Б.

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

Спочатку JUnit створює екземпляри testClass для кожного методу тестування та починає виконувати тести після створення кожного екземпляра. Перед запуском тестового методу запускається його метод налаштування, в якому можна підготувати деякий стан.

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

Життєвий цикл JUnits:

  1. Створіть інший екземпляр тестового класу для кожного методу тестування
  2. Повторіть для кожного екземпляра тестового класу: налаштування виклику + виклик методу тестування

З деякими журналами в тесті з двома методами тестування ви отримуєте: (номер - хеш-код)

  • Створення нового примірника: 5718203
  • Створення нового примірника: 5947506
  • Налаштування: 5718203
  • TestOne: 5718203
  • Установка: 5947506
  • TestTwo: 5947506

3
Правильно, але поза темою. База даних по суті є глобальним станом. Це не проблема, з якою я стикаюся. Мене просто хвилює швидкість виконання належних незалежних тестів.
Крейг П. Мотлін

Цей порядок ініціалізації справедливий лише у JUnit 3, де це важлива обережність. У JUnit 4 тестові екземпляри створюються ліниво, тому ініціалізація в декларації або методі настройки обидва відбувається в час тестування. Також для одноразового налаштування можна скористатися @BeforeClassв JUnit 4.
Nils von Barth,

11

У JUnit 4:

  • Для класу під тестом ініціалізуйте @Beforeметод, щоб визначити невдачі.
  • Для інших класів ініціалізуйте в декларації ...
    • ... для стислості та позначення полів final, точно так, як зазначено в запитанні,
    • ... якщо тільки це не є складною ініціалізацією, яка могла б зазнати невдачі, в такому випадку використати @Before, щоб знайти помилки.
  • Для глобального стану (особливо повільна ініціалізація , як база даних) використовуйте @BeforeClass, але будьте обережні щодо залежностей між тестами.
  • Ініціалізація об'єкта, що використовується в одному тесті, звичайно, повинна проводитися в самому методі тестування.

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

Це прийнятно , щоб мати простий стандарт і завжди використовувати @Before(явні помилки , але багатослівно) або завжди ініціалізації в декларації (короткому , але дає помилку помилки), так як складні правила кодування важко слідувати, і це не має велике значення .

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

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


7

У JUnit 3 ваші ініціалізатори поля будуть запускатися один раз за методом тестування перед тим, як виконувати будь-які тести . Поки ваші значення поля мають невелику пам’ять, забирайте небагато часу та не впливайте на глобальний стан, використання польових ініціалізаторів технічно добре. Однак, якщо це не вдається, ви, можливо, споживаєте багато пам'яті або часу, налаштовуючи свої поля до початку першого тесту, і, можливо, навіть не вистачає пам'яті. З цієї причини багато розробників завжди встановлюють значення поля в методі setUp (), де це завжди безпечно, навіть коли це не суворо необхідно.

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


Цікаво. Тож поведінка, яку ви описали спочатку, стосується лише JUnit 3?
Крейг П. Мотлін

6

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


1
добре сказано. Просто звикайте завжди створювати інстанції в setUp (), і у вас є питання, що менше турбуватися - наприклад, де я повинен інстанціювати свій fooBar, де моя колекція. Це свого роду стандарт кодування, якого просто потрібно дотримуватися. Вигідно вам не зі списками, а з іншими моментами.
Олаф Кок

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

5

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

Приклад оптимізації за допомогою @BeforeClassметоду настройки: я використовую dbunit для деяких функціональних тестів бази даних. Метод налаштування відповідає за приведення бази даних у відомий стан (дуже повільно ... 30 секунд - 2 хвилини залежно від кількості даних). Я завантажую ці дані в описуваному методі настройки, @BeforeClassа потім виконую 10-20 тестів проти того ж набору даних, на відміну від повторної завантаження / ініціалізації бази даних у кожному тесті.

Використання Junit 3.8 (розширення TestCase, як показано у вашому прикладі) вимагає написати трохи більше коду, ніж просто додати анотацію, але "запустити один раз перед встановленням класу" все ж можливо.


1
+1, тому що я також віддаю перевагу читанню. Однак я не переконаний, що другий спосіб - це оптимізація взагалі.
Крейг П. Мотлін

@Motlin Я додав приклад dbunit, щоб уточнити, як можна оптимізувати налаштування.
Алекс Б

База даних по суті є глобальним станом. Тому переміщення налаштування db на setUp () не є оптимізацією, тести повинні завершитись належним чином.
Крейг П. Мотлін

@ Алекс В: Як сказав Мотлін, це не оптимізація. Ви просто змінюєте, де в коді робиться ініціалізація, але не скільки разів, ні як швидко.
Едді

Я мав на увазі використовувати анотацію "@BeforeClass". Редагування прикладу для уточнення.
Алекс Б

2

Оскільки кожен тест виконується незалежно, зі свіжим екземпляром об'єкта, тест-об’єкт, який має будь-який внутрішній стан, крім того, що поділяється між, setUp()та окремим тестом та tearDown(). Це одна з причин (на додаток до причин, які наводили інші), що добре використовувати setUp()метод.

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

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

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


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

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

Перевага, finalоднак, згадується у питанні.
Нілс фон Барт

0
  • Постійні значення (використання у світильниках або твердженнях) слід ініціалізувати у своїх деклараціях та final(як ніколи не змінювати)

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

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

Тест без залежності для глузування може виглядати так:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Тест із залежностями для ізоляції може виглядати так:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

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