Лучшая практика: инициализировать поля класса 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
    }
}

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

Уточнение: JUnit будет создавать экземпляр тестового класса один раз для каждого тестового метода. Это означает, listчто будет создаваться один раз за тест, независимо от того, где я это объявляю. Это также означает, что между тестами нет временных зависимостей. Похоже, что в использовании setUp () нет никаких преимуществ. Однако в JUnit FAQ есть много примеров, которые инициализируют пустую коллекцию в setUp (), поэтому я полагаю, что для этого должна быть причина.

Крейг П. Мотлин
источник
2
Помните, что ответ отличается в JUnit 4 (инициализировать в объявлении) и JUnit 3 (использовать setUp); в этом корень путаницы.
Нильс фон Барт,
См. Также stackoverflow.com/questions/6094081/…
Григорий Кислин

Ответы:

99

Если вас интересуют конкретные примеры в FAQ JUnit, такие как базовый тестовый шаблон , я думаю, что лучшая практика, демонстрируемая там, заключается в том, что тестируемый класс должен быть создан в вашем методе setUp (или в тестовом методе) ,

Когда примеры JUnit создают ArrayList в методе setUp, все они переходят к тестированию поведения этого ArrayList с такими случаями, как testIndexOutOfBoundException, testEmptyCollection и т.п. Есть перспектива того, что кто-то напишет класс и убедится, что он работает правильно.

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

С другой стороны, если вы используете класс коллекции Java (или другой класс библиотеки, если на то пошло) в своем тестовом коде, это, вероятно, не потому, что вы хотите его протестировать - это просто часть тестового приспособления. В этом случае вы можете с уверенностью предположить, что он работает по назначению, поэтому инициализировать его в объявлении не составит труда.

Как бы то ни было, я работаю над достаточно большой кодовой базой, разработанной TDD уже несколько лет назад. Обычно мы инициализируем объекты в их объявлениях в тестовом коде, и за те полтора года, что я работаю над этим проектом, это ни разу не вызвало проблем. Так что есть по крайней мере некоторые анекдотические свидетельства того, что это разумный поступок.

Мох Коллум
источник
45

Я начал копать сам и нашел одно потенциальное преимущество использования setUp(). Если во время выполнения возникнут какие-либо исключения setUp(), JUnit напечатает очень полезную трассировку стека. С другой стороны, если во время построения объекта возникает исключение, в сообщении об ошибке просто говорится, что JUnit не смог создать экземпляр тестового примера, и вы не видите номер строки, в которой произошел сбой, вероятно, потому, что JUnit использует отражение для создания экземпляра теста. классы.

Ничего из этого не применимо к примеру создания пустой коллекции, поскольку она никогда не будет выбрана, но это преимущество setUp()метода.

Крейг П. Мотлин
источник
18

В дополнение к ответу Алекса Б.

Требуется даже использовать метод setUp для создания экземпляров ресурсов в определенном состоянии. Выполнение этого в конструкторе - это не только вопрос времени, но и из-за того, как JUnit запускает тесты, каждое состояние теста будет стерто после его запуска.

JUnit сначала создает экземпляры testClass для каждого метода тестирования и запускает тесты после создания каждого экземпляра. Перед запуском метода тестирования запускается метод его настройки, в котором можно подготовить некоторое состояние.

Если состояние базы данных будет создано в конструкторе, все экземпляры будут создавать экземпляры состояния базы данных сразу после друг друга перед запуском каждого теста. Что касается второго теста, тесты будут выполняться с грязным состоянием.

Жизненный цикл JUnits:

  1. Создайте отдельный экземпляр тестового класса для каждого метода тестирования
  2. Повторите для каждого экземпляра тестового класса: вызовите setup + вызовите testmethod

С некоторыми входами в тест с двумя методами тестирования вы получите: (число - это хэш-код)

  • Создание нового экземпляра: 5718203
  • Создание нового экземпляра: 5947506
  • Настройка: 5718203
  • TestOne: 5718203
  • Настройка: 5947506
  • TestTwo: 5947506
Юрген Ханнаерт
источник
3
Правильно, но не по теме. База данных - это по сути глобальное состояние. Это не проблема, с которой я сталкиваюсь. Меня просто беспокоит скорость выполнения правильно независимых тестов.
Крейг П. Мотлин
Этот порядок инициализации верен только в JUnit 3, где это важное предупреждение. В JUnit 4 тестовые экземпляры создаются лениво, поэтому инициализация в объявлении или в методе настройки происходит во время тестирования. Также для одноразовой настройки можно использовать @BeforeClassв JUnit 4.
Нильс фон Барт,
11

В JUnit 4:

  • Для тестируемого класса инициализируйте @Beforeметод для обнаружения сбоев.
  • Для других классов инициализируйте в объявлении ...
    • ... для краткости и пометить поля finalточно так, как указано в вопросе,
    • ... если только это не сложная инициализация, которая может дать сбой, и в этом случае используйте @Beforeдля обнаружения сбоев.
  • Для глобального состояния (особенно медленной инициализации , например, базы данных) используйте @BeforeClass, но будьте осторожны с зависимостями между тестами.
  • Инициализация объекта, используемого в одном тесте, конечно же, должна выполняться в самом методе тестирования.

Инициализация @Beforeметода или метода тестирования позволяет улучшить отчеты об ошибках. Это особенно полезно для создания экземпляра тестируемого класса (который вы можете сломать), но также полезно для вызова внешних систем, таких как доступ к файловой системе («файл не найден») или подключение к базе данных («соединение отклонено»).

Это приемлемо , чтобы иметь простой стандарт и всегда использовать @Before(явные ошибки , но многословно) или всегда инициализации в декларации (кратком , но дает ошибку заблуждения), так как сложные правила кодирования трудно следовать, и это не имеет большое значение .

Инициализация в setUp- это пережиток JUnit 3, где все тестовые экземпляры были инициализированы с нетерпением, что вызывает проблемы (скорость, память, исчерпание ресурсов), если вы выполняете дорогостоящую инициализацию. Таким образом, лучше всего было выполнить дорогостоящую инициализацию в setUp, которая запускалась только при выполнении теста. Это больше не применяется, поэтому использовать его гораздо меньше setUp.

Это суммирует несколько других ответов, которые похоронили lede, в частности, Крейга П. Мотлина (сам вопрос и ответ сам), Moss Collum (тестируемый класс) и dsaff.

Нильс фон Барт
источник
7

В JUnit 3 ваши инициализаторы полей будут запускаться один раз для каждого метода тестирования перед запуском любых тестов. . Пока ваши значения полей невелики в памяти, занимают мало времени на настройку и не влияют на глобальное состояние, использование инициализаторов полей технически нормально. Однако, если они не выполняются, вы можете в конечном итоге потребить много памяти или времени на настройку полей до запуска первого теста и, возможно, даже нехватить памяти. По этой причине многие разработчики всегда устанавливают значения полей в методе setUp (), где это всегда безопасно, даже если это не является строго необходимым.

Обратите внимание, что в JUnit 4 инициализация тестового объекта происходит прямо перед запуском теста, поэтому использование инициализаторов полей является более безопасным и рекомендуемым стилем.

dsaff
источник
Интересный. Итак, поведение, которое вы описали сначала, применимо только к JUnit 3?
Craig P. Motlin 02
6

В вашем случае (создание списка) на практике разницы нет. Но в целом лучше использовать setUp (), потому что это поможет Junit правильно сообщать об исключениях. Если в конструкторе / инициализаторе теста возникает исключение, это означает сбой теста . Однако, если во время установки возникает исключение, естественно думать об этом как о некоторой проблеме при настройке теста, и junit сообщает об этом соответствующим образом.

Амит
источник
1
хорошо сказано. Просто привыкните всегда создавать экземпляры в setUp (), и у вас будет на один вопрос меньше, о котором стоит беспокоиться - например, где я должен создавать мой fooBar, где моя коллекция. Это своего рода стандарт кодирования, которого нужно просто придерживаться. Выгодно не для списков, а для других экземпляров.
Олаф Кок
@Olaf Спасибо за информацию о стандарте кодирования, я не думал об этом. Однако я склонен больше соглашаться с идеей Мосса Коллума о стандарте кодирования.
Крейг П. Мотлин,
5

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

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

Использование Junit 3.8 (расширение TestCase, как показано в вашем примере) требует написания немного большего количества кода, чем просто добавление аннотации, но «выполнить один раз перед установкой класса» все еще возможно.

Алекс Б
источник
1
+1 потому что я тоже предпочитаю читабельность. Однако я не уверен, что второй способ - это вообще оптимизация.
Крейг П. Мотлин
@Motlin Я добавил пример dbunit, чтобы прояснить, как можно оптимизировать с помощью настройки.
Alex B
База данных - это по сути глобальное состояние. Таким образом, перемещение настройки db в setUp () не является оптимизацией, это необходимо для правильного выполнения тестов.
Крейг П. Мотлин
@Alex B: Как сказал Мотлин, это не оптимизация. Вы просто меняете место в коде, где выполняется инициализация, но не то, сколько раз и как быстро.
Эдди
Я имел в виду использование аннотации «@BeforeClass». Редактируем пример для пояснения.
Alex B
2

Поскольку каждый тест выполняется независимо, с новым экземпляром объекта, нет особого смысла в том, чтобы объект Test имел какое-либо внутреннее состояние, кроме общего между setUp()и отдельным тестом и tearDown(). Это одна из причин (в дополнение к другим причинам) того, что использовать этот setUp()метод полезно .

Примечание. Поддерживать статическое состояние тестовым объектом JUnit - плохая идея! Если вы используете статическую переменную в своих тестах для чего-либо, кроме целей отслеживания или диагностики, вы лишаете законной силы часть цели JUnit, заключающуюся в том, что тесты могут (а могут) выполняться в любом порядке, причем каждый тест выполняется с свежее, чистое состояние.

Преимущества использования заключаются в 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()
         ...
    }
}
davidxxx
источник