Мне действительно нужен модуль модульного тестирования?

19

В настоящее время на моей работе у нас есть большой набор модульных тестов для нашего приложения C ++. Однако мы не используем фреймворк для юнит-тестирования. Они просто используют макрос C, который в основном обертывает assert и cout. Что-то вроде:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Затем мы просто создаем функции для каждого из наших тестов, такие как

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Наш CI-сервер обнаруживает «модульный тест не пройден» и завершает сборку, отправляя сообщение по электронной почте разработчикам.

И если у нас есть дубликат установочного кода, мы просто реорганизуем его так же, как и любой другой дублированный код, который мы имеем в производстве. Мы обертываем его вспомогательными функциями, заставляем некоторые тестовые классы оборачивать, настраивая часто используемые сценарии.

Я знаю, что есть фреймворки, такие как CppUnit и буст-модульный тест. Мне интересно, какую ценность они добавляют? Я скучаю по тому, что они приносят на стол? Есть ли что-то полезное, что я мог бы извлечь из них? Я не решаюсь добавить зависимость, если она не добавляет реальной ценности, особенно если учесть, что то, что у нас есть, очень просто и работает хорошо.

Дуг Т.
источник

Ответы:

8

Как уже говорили другие, у вас уже есть свой собственный, простой, самодельный каркас.

Это кажется тривиальным, чтобы сделать один. Тем не менее, есть некоторые другие особенности инфраструктуры модульных тестов, которые не так легко реализовать, потому что они требуют некоторых дополнительных знаний языка. Функции, которые я обычно требую от тестовой среды и которые не так просты для доморощенного:

  • Автоматический сбор тестовых случаев. Т.е. определения нового метода тестирования должно быть достаточно для его выполнения. JUnit автоматически собирает все методы, имена которых начинаются с test, NUnit имеет [Test]аннотацию, Boost.Test использует BOOST_AUTO_TEST_CASEи BOOST_FIXTURE_TEST_CASEмакросы.

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

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

    Скажем, вы только что получили сообщение об ошибке # 4211, и его можно воспроизвести с помощью модульного теста. Итак, вы пишете один, но затем вам нужно указать бегуну, чтобы он выполнял только этот тест, чтобы вы могли отладить то, что на самом деле не так.

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

    Любой тестовый набор приличного размера будет иметь тесты, которые не пройдены, потому что тестируемые им функции еще не были реализованы, еще не завершены, никто не успел их исправить или еще что-то. Без возможности помечать тесты как ожидаемые сбои, вы не заметите еще один сбой, если они есть регулярно, поэтому тесты перестают служить своей основной цели.

Ян Худек
источник
спасибо, я думаю, что это лучший ответ. Сейчас мой макрос выполняет свою работу, но я не могу использовать ни одну из функций, о которых вы упомянули.
Дуг Т.
1
@Jan Hudec "Это в основном удобство, но каждое удобство, которое вы можете получить, повышает вероятность того, что разработчики действительно напишут тесты, которые они должны, и что они подключат их правильно."; Все рамки тестирования (1) нетривиальны для установки, часто содержат более устаревшие или неисчерпывающие инструкции по установке, чем актуальные действующие инструкции; (2) если вы фиксируете тестовый фреймворк напрямую, без интерфейса в середине, вы женаты на нем, переключение фреймворков не всегда легко.
Дмитрий
@Jan Hudec Если мы ожидаем, что больше людей будут писать модульные тесты, у нас должно быть больше результатов в Google для «Что такое модульный тест», чем «Что такое модульное тестирование». Нет смысла проводить юнит-тестирование, если вы не знаете, что юнит-тест не зависит от каких-либо структур юнит-тестирования или определения юнит-тестирования. Вы не можете проводить модульное тестирование, если у вас нет четкого понимания того, что такое модульное тестирование, поскольку в противном случае нет смысла проводить модульное тестирование.
Дмитрий
Я не покупаю этот аргумент удобства. Написание тестового кода очень сложно, если вы покинете тривиальный мир примеров. Все это макеты, настройки, библиотеки, внешние серверные программы макетов и т. Д. Все они требуют, чтобы вы знали структуру тестирования изнутри.
Лотар
@ Лотар, да, это много работы и многому нужно научиться, но все же приходится писать простой шаблон снова и снова, потому что вам не хватает пары полезных утилит, что делает работу намного менее приятной, и это делает заметную разницу в эффективности.
Ян Худек
27

Похоже, вы уже используете каркас, самодельный.

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

С другой стороны, самодельная инфраструктура вынуждает вас либо никогда не делиться своим кодом, либо предоставлять саму платформу, что может стать обременительным с ростом самой платформы.

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

Вторым недостатком самодельных фреймворков является совместимость . Популярные платформы модульных тестов, как правило, обеспечивают совместимость с различными IDE, системами контроля версий и т. Д. На данный момент это может быть не очень важно для вас, но что произойдет, если однажды вам потребуется что-то изменить на сервере CI или перенести новой IDE или новой VCS? Будете ли вы изобретать велосипед?

И последнее, но не менее важное: более крупные платформы предоставляют больше возможностей, которые вам, возможно, потребуется реализовать в своей собственной среде за один день. Assert.AreEqual(expected, actual)не всегда достаточно Что делать, если вам нужно:

  • измерить точность?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • пустой тест, если он работает слишком долго? Реализация таймаута может быть непростой даже в языках, которые облегчают асинхронное программирование.

  • проверить метод, который ожидает исключения?

  • есть более элегантный код?

    Assert.Verify(a == null);
    

    хорошо, но разве это не более выразительно в ваших намерениях написать следующую строку?

    Assert.IsNull(a);
    
Арсений Мурзенко
источник
Все «рамки», которые мы используем, находятся в очень маленьком заголовочном файле и соответствуют семантике assert. Так что я не слишком беспокоюсь о недостатках, которые вы перечислите.
Дуг Т.
4
Я считаю утверждения самой тривиальной частью тестового фреймворка. Бегун, который собирает и запускает контрольные примеры и проверяет результаты, является нетривиальной важной частью.
Ян Худек
@ Ян, я не совсем понимаю. Мой бегун - это основная процедура, общая для каждой программы на C ++. Бегун фреймворка модульного тестирования делает что-то более сложное и полезное?
Дуг Т.
1
Ваш фреймворк учитывает только семантику утверждения и выполнения тестов в основном методе ... пока. Просто подождите, пока вы не сгруппируете свои утверждения в несколько сценариев, сгруппируйте связанные сценарии вместе на основе инициализированных данных и т. Д.
Джеймс Кингсбери
@ DougT .: Да, приличный бегун фреймворка юнит-тестов делает некоторые более сложные полезные вещи Смотрите мой полный ответ.
Ян Худек
4

Как уже говорили другие, у вас уже есть свои, домашние рамки.

Единственная причина, по которой я вижу использование какой-либо другой тестовой среды, может быть связана с отраслевыми "общими знаниями". Новым разработчикам не придется изучать ваш домашний способ (хотя это выглядит очень просто).

Кроме того, другие тестовые среды могут иметь больше возможностей, которыми вы можете воспользоваться.

Ozz
источник
1
Согласовано. Если вы не сталкиваетесь с ограничениями вашей текущей стратегии тестирования, я вижу мало причин для изменений. Хорошая структура, вероятно, обеспечит лучшие возможности организации и отчетности, но вам придется оправдать дополнительную работу, необходимую для интеграции с вашей базой кода (включая вашу систему сборки).
TMN
3

У вас уже есть фреймворк, даже если он простой.

Основными преимуществами большей структуры, как я их вижу, является возможность иметь много различных видов утверждений (таких как подтверждение повышения), логический порядок для модульных тестов и возможность запускать только подмножество модульных тестов в время. Также неплохо следовать шаблону тестов xUnit, например, setUP () и tearDown (). Конечно, это блокирует вас в указанных рамках. Обратите внимание, что некоторые фреймворки имеют лучшую интеграцию интеграции, чем другие - например, Google mock и test.

Сколько времени вам понадобится, чтобы перестроить все ваши юнит-тесты на новый фреймворк? Дни или несколько недель могут стоить того, но больше, может быть, не так много.

Сардатрион - Восстановить Монику
источник
2

На мой взгляд, у вас обоих есть преимущество, и вы находитесь в «невыгодном положении» (так).

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

Одним из преимуществ хорошо спроектированного API модульного тестирования является то, что в большинстве современных IDE имеется много встроенной поддержки. Это не повлияет на пользователей ядра VI и emacs, которые насмехаются над пользователями Visual Studio, но для тех, кто использует хорошую IDE, у вас есть возможность отлаживать свои тесты и выполнять их внутри сама IDE. Это хорошо, однако есть еще большее преимущество в зависимости от используемой платформы, и это на языке, используемом для тестирования вашего кода.

Когда я говорю « язык» , я не говорю о языке программирования, но вместо этого я говорю о богатом наборе слов, заключенном в свободный синтаксис, который делает тестовый код читаемым как история. В частности, я стал сторонником использования структур BDD . Мой личный любимый API DotNet BDD - StoryQ, но есть несколько других с той же основной целью, которая заключается в том, чтобы вынуть концепцию из документа требований и записать ее в коде аналогично тому, как это написано в спецификации. Однако действительно хорошие API-интерфейсы идут еще дальше, перехватывая каждую отдельную инструкцию в тесте и указывая, успешно ли выполнен этот оператор или нет. Это невероятно полезно, поскольку вы видите, как весь тест выполняется без раннего возврата, что означает, что ваши усилия по отладке становятся невероятно эффективными, поскольку вам нужно только сосредоточить свое внимание на тех частях теста, которые не прошли, без необходимости декодировать весь вызов последовательность. Еще одна приятная вещь заключается в том, что результаты теста показывают вам всю эту информацию,

В качестве примера того, о чем я говорю, сравните следующее:

Используя Утверждения:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Использование свободного API BDD: (представьте, что выделенные курсивом биты являются в основном указателями метода)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Теперь, когда синтаксис BDD более длинный и многословный, и эти примеры ужасно надуманы, однако для очень сложных ситуаций тестирования, когда в системе происходит много изменений в результате заданного поведения системы, синтаксис BDD предлагает вам четкий описание того, что вы тестируете, и как была определена ваша тестовая конфигурация, и вы можете показать этот код непрограммисту, и он сразу же поймет, что происходит. Кроме того, если «variable_A» не пройдёт тест в обоих случаях, пример Asserts не будет выполняться после первого утверждения, пока вы не исправите проблему, в то время как API BDD, в свою очередь, выполнит каждый метод, вызванный в цепочке, и укажет, какой отдельные части заявления были по ошибке.

Лично я считаю, что этот подход работает намного лучше, чем более традиционные фреймворки xUnit, в том смысле, что язык тестирования - это тот же язык, на котором ваши клиенты будут говорить о своих логических требованиях. Несмотря на это, мне удалось использовать фреймворки xUnit в аналогичном стиле, при этом мне не нужно было изобретать полный API-интерфейс тестирования для поддержки моих усилий, и хотя утверждения по-прежнему будут эффективно закорачивать себя, они читают более чисто. Например:

Используя Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Если вы решите исследовать с помощью API модульного тестирования, я советую немного поэкспериментировать с большим количеством различных API, и не забывать о своем подходе. Хотя я лично выступаю за BDD, потребности вашего бизнеса могут потребовать чего-то другого в зависимости от обстоятельств вашей команды. Ключ, однако, заключается в том, чтобы избежать повторного угадывания существующей системы. Вы всегда можете поддержать свои существующие тесты несколькими тестами, используя другой API, если это необходимо, но я, конечно, не рекомендую огромную переписать тест, чтобы сделать все то же самое. Поскольку устаревший код перестает использоваться, вы можете легко заменить его и его тесты новым кодом, а также тестировать с использованием альтернативного API, и это без необходимости вкладывать большие усилия, которые не обязательно принесут вам реальную ценность для бизнеса. Что касается использования API модульного тестирования,

S.Robins
источник
1

То, что у вас есть, просто и выполняет свою работу. Если это работает для вас, отлично. Вам не нужна основная структура модульного тестирования, и я бы не стал переходить к работе по переносу существующей библиотеки модульных тестов на новую среду. Я думаю, что самая большая ценность фреймворков модульного тестирования - это уменьшить барьер для входа; вы только начинаете писать тесты, потому что фреймворк уже на месте. Вы прошли эту точку, поэтому вы не получите эту выгоду.

Другое преимущество использования основной платформы - и это незначительное преимущество, IMO - заключается в том, что новые разработчики уже могут освоить любую среду, которую вы используете, и, следовательно, потребуют меньше обучения. На практике с простым подходом, подобным тому, что вы описали, это не должно иметь большого значения.

Кроме того, большинство основных платформ имеют определенные функции, которые ваша платформа может иметь или не иметь. Эти функции сокращают объем программного кода, а также ускоряют и упрощают написание тестовых случаев:

  • Автоматический запуск контрольных примеров с использованием соглашений об именах, аннотаций / атрибутов и т. Д.
  • Различные более конкретные утверждения, так что вам не нужно писать условную логику для всех ваших утверждений или перехватывать исключения, чтобы утверждать их тип.
  • Категоризация тестовых случаев, так что вы можете легко запускать их подмножества.
Адам Яскевич
источник