Написание первого теста JUnit

Итак, я прочитал официальные документы JUnit, в которых содержится множество примеров, но (как и во многих других случаях) у меня есть Eclipse, и я пишу свой первый тест JUnit, и я задыхаюсь от какого-то базового дизайна/концептуальные проблемы.

Итак, если мой WidgetUnitTest тестирует цель с именем Widget, я предполагаю, что мне нужно будет создать справедливое число Widget для использования во всех методах тестирования. Должен ли я конструировать эти Widget в конструкторе WidgetUnitTest или в методе setUp()? Должно ли быть отношение 1:1 Widget к методам тестирования или использовать лучшие методы, чтобы максимально использовать Widget?

Наконец, насколько гранулярность должна существовать между утверждениями/неудачами и методами тестирования? Пурист может утверждать, что в методе тестирования должны существовать утверждения 1 и только 1, однако в рамках этой парадигмы, если Widget имеет геттер, называемый getBuzz(), я получаю 20 различных методов тестирования для getBuzz() с именами типа

@Test
public void testGetBuzzWhenFooIsNullAndFizzIsNonNegative() { ... }

В отличие от одного метода, который проверяет множество сценариев и содержит множество утверждений:

@Test
public void testGetBuzz() { ... }

Спасибо за понимание некоторых мастеров JUnit!

Ответ 1

шаблон

Интересный вопрос. Прежде всего - мой окончательный тестовый шаблон, сконфигурированный в среде IDE:

@Test
public void shouldDoSomethingWhenSomeEventOccurs() throws Exception
{
    //given

    //when

    //then
}

Я всегда начинаю с этого кода (умные люди называют его BDD).

  • В given я устанавливаю тестовую установку уникальной для каждого теста.

  • when - идеальная линия - вещь, которую вы тестируете.

  • then должен содержать утверждения.

Я не один адвокат утверждения, однако вы должны проверить только один аспект поведения. Например, если метод должен что-то вернуть, а также имеет некоторые побочные эффекты, создайте два теста с теми же разделами given и when.

Также тестовый шаблон включает throws Exception. Это должно обрабатывать раздражающие проверенные исключения в Java. Если вы протестируете какой-то код, который их бросает, вы не будете обеспокоены компилятором. Конечно, если тест выдает исключение, он терпит неудачу.

Настройка

Настройка теста очень важна. С одной стороны, разумно извлечь общий код и поместить его в метод setup()/@Before. Однако обратите внимание, что при чтении теста (и читаемость - самое большое значение в модульном тестировании!) легко пропустить установочный код, висящий где-то в начале тестового примера. Поэтому соответствующая тестовая установка (например, вы можете создавать виджет по-разному) должна перейти к методу тестирования, но необходимо извлечь инфраструктуру (настройку общих макетов, запуск встроенной тестовой базы данных и т.д.). Еще раз для повышения удобочитаемости.

Также вы знаете, что JUnit создает новый экземпляр класса тестового случая для каждого теста? Поэтому, даже если вы создаете свой CUT (класс под тестированием) в конструкторе, конструктор вызывается перед каждым тестом. Вид раздражает.

Зернистость

Сначала введите свой тест и подумайте, какой прецедент или функциональность вы хотите протестировать, никогда не думайте в терминах:

это класс Foo, имеющий методы bar() и buzz(), поэтому я создаю FooTest с помощью testBar() и testBuzz(). О, дорогая, мне нужно протестировать два пути выполнения на протяжении bar() - поэтому создадим testBar1() и testBar2().

shouldTurnOffEngineWhenOutOfFuel() хорошо, testEngine17() плохо.

Подробнее об именах

Что говорит об этом тесте testGetBuzzWhenFooIsNullAndFizzIsNonNegative? Я знаю, что это что-то проверяет, но почему? И разве вы не думаете, что детали слишком интимные? Как насчет:

@Test shouldReturnDisabledBuzzWhenFooNotProvidedAndFizzNotNegative`

Он описывает ввод значимым образом и ваши намерения (при условии, что отключенный шум является своего рода buzz статус/тип). Также обратите внимание, что мы больше не кодируем getBuzz() имя метода и null contract для Foo (вместо этого мы говорим: когда Foo не предоставляется). Что делать, если в будущем вы замените null нулевым шаблоном объекта?

Также не бойтесь 20 различных методов тестирования для getBuzz(). Вместо этого подумайте о 20 различных случаях использования, которые вы тестируете. Однако, если класс тестового теста становится слишком большим (поскольку он обычно намного больше, чем тестируемый класс), извлеките его в несколько тестовых примеров. Еще раз: FooHappyPathTest, FooBogusInput и FooCornerCases хороши, Foo1Test и Foo2Test плохие.

читаемость

Стремитесь к коротким и описательным именам. Несколько строк в given и несколько в then. Это. Создавайте сборщики и внутренние DSL файлы, извлекайте методы, пишите пользовательские совпадения и утверждения. Тест должен быть даже более читаемым, чем производственный код. Не переусердствуйте.

Мне полезно сначала написать серию пустых хорошо известных методов тестовых случаев. Затем я возвращаюсь к первому. Если я до сих пор понимаю, что я должен тестировать при каких условиях, я реализую тестовое построение API класса за это время. Затем я реализую этот API. Умные люди называют это TDD (см. Ниже).

Рекомендуемое значение:

Ответ 2

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

Я бы рекомендовал провести отдельный тест для каждого сценария/поведения/логического потока, который вам нужно проверить, а не один массивный тест для всего в getBuzz(). Вы хотите, чтобы каждый тест имел целенаправленную цель того, что вы хотите проверить в getBuzz().

Ответ 3

Вместо методов тестирования старайтесь сосредоточиться на тестировании поведения. Задайте вопрос: "Что должен делать виджет?" Затем напишите тест, подтверждающий ответ. Например. "Виджет должен нервничать"

public void setUp() throws Exception {
   myWidget = new Widget();
}

public void testAWidgetShouldFidget() throws Exception {
  myWidget.fidget();
}

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

public void setUp() throws Exception {
   //Given a widget
   myWidget = new Widget();
   //And it original position
   Point initialWidgetPosition = widget.position();
}


public void testAWidgetShouldFidget() throws Exception {
  myWidget.fidget();
}

public void testAWidgetPositionShouldChangeWhenItFidgets() throws Exception {
  myWidget.fidget();
  assertNotEquals(initialWidgetPosition, widget.position());
}

Некоторые утверждают, что оба теста выполняют одно и то же поведение fidget, но имеет смысл выделить поведение fidget независимо от того, как оно влияет на widget.position(). Если одно поведение прерывается, один тест будет определять причину сбоя. Также важно указать, что поведение может осуществляться само по себе как выполнение спецификации (у вас есть программные спецификации, не так ли?), Что предполагает, что вам нужен неспокойный виджет. В конце концов, все это касается реализации ваших программных спецификаций как кода, который использует ваши интерфейсы, которые демонстрируют, что вы завершили спецификацию, а во-вторых, как один взаимодействует с вашим продуктом. Это по сути, как TDD должен работать. Любая попытка разрешить ошибки или протестировать продукт обычно приводит к разочаровывающим бессмысленным дебатам о том, какие рамки использовать, уровень охвата и насколько тонкий размер вашего пакета должен быть. Каждый тестовый пример должен состоять в том, чтобы разбить вашу спецификацию на компонент, где вы можете начинать формулировку с помощью "Дано/когда/потом". Учитывая {какое-либо состояние приложения или предварительное условие} Когда {поведение вызывается} Затем {утверждать некоторый наблюдаемый вывод}.

Ответ 4

Прежде всего, методы setUp и tearDown будут вызываться до и после каждого теста, поэтому метод setUp должен создавать объекты, если они вам понадобятся в каждом тесте, и конкретные тесты могут быть выполнены в тесте сам.

Во-вторых, зависит от вас, как вы хотите протестировать свою программу. Очевидно, что вы могли бы написать тест для каждой возможной ситуации в своей программе и в итоге получить тесты gazillion для каждого метода. Или вы можете написать только один тест для каждого метода, который проверяет все возможные сценарии. Я бы рекомендовал смесь между обоими способами. Вам действительно не нужен тест для тривиальных getters/seters, но написание только одного теста для метода может привести к путанице, если тест не удастся. Вы должны решить, какие методы стоит проверить, и какие сценарии заслуживают тестирования. Но в принципе каждый сценарий должен иметь свой собственный тест.

В основном я заканчиваю с охватом кода от 80 до 90 процентов с моими испытаниями.

Ответ 5

Я полностью второй Томаш Нуркевич отвечаю, поэтому я скажу, что вместо того, чтобы повторять все, что он сказал.

Еще пару очков:

Не забудьте проверить ошибки. Вы можете считать что-то вроде этого:

@Test
public void throwExceptionWhenConditionOneExist() {
    // setup
    // ...
    try {
       classUnderTest.doSomething(conditionOne);
       Assert.fail("should have thrown exception");
    } catch (IllegalArgumentException expected) {
       Assert.assertEquals("this is the expected error message", expected.getMessage());
    } 
}

Кроме того, у него есть БОЛЬШОЕ значение, чтобы начать писать тесты, прежде чем даже подумать о дизайне вашего тестируемого класса. Если вы новичок в модульном тестировании, я не могу подчеркнуть достаточно обучения этому методу в одно и то же время (это называется TDD, тестовое развитие), которое продолжается следующим образом:

  • Вы думаете о том, что у вас есть для пользовательских требований.
  • Вы пишете базовый первый тест для него
  • Вы можете скомпилировать его (создав необходимые классы, включая ваш класс под тестом, и т.д.).
  • Вы запускаете его: он должен потерпеть неудачу
  • Теперь вы реализуете функциональность тестируемого класса, которая заставит его пройти (и ничего больше)
  • Промыть и повторить с новым требованием

Когда все ваши требования проходят тесты, вы закончите. Вы НИКОГДА не пишете ничего в своем производственном коде, который раньше не тестировался (исключения из этого - это код регистрации и не намного больше).

TDD имеет неоценимое значение при разработке кода хорошего качества, а не в требованиях, связанных с надстройкой, и обеспечения 100% функционального покрытия (а не покрытия линии, что обычно бессмысленно). Это требует изменения в способе, которым вы считаете кодирование, поэтому полезно изучить технику одновременно с тестированием. Как только вы его получите, это станет естественным.

Следующий шаг изучает насмешливые стратегии:)

Получите удовольствие от тестирования.