Главная Новости

Юнит-тестирование. Пример. Boost Unit Test

Опубликовано: 01.09.2018

видео Юнит-тестирование. Пример. Boost Unit Test

Зачем нужно изучать и писать юнит-тесты

Разработка и поддержка программ невозможна без внесения изменений в существующий код. Однако, всякое изменение сопряжено с возможным внесением ошибок. Чем больше и сложнее проект — тем более нетривиальным образом изменения могут сказываться на работе подсистем. В связи с этим, любое изменение кода требует проведения тестирования. В статье описываются:



теория unit-тестирования; Unit Test Framework библиотеки Boost; пример разработки программы с использованием unit-тестирования.

1 Цели тестирования

Тестирование — процесс исследования программного обеспечения с целью выявления ошибок. Однако, тесты могут использоваться и в других целях:


Unit тестирование в С#. Как создать Unit тест в C#

как документация. Тесты отражают ожидаемую реакцию системы на какое-либо воздействие. Информация, записанная в виде тестов, может быть полезна при ознакомлении с устройством программы (например, при попадании в проект нового разработчика); для ознакомления с проектом программист может покрыть код тестами (так называемыми «учебными тестами«). При этом разработчик сформулирует свои ожидания к коду (в виде наборов тестовых данных) и проверит их корректность; в случае использования сторонних библиотек можно натолкнуться на неприятности при обновлении. Разработчики библиотек могут исправить какие-либо ошибки в своем коде, но это может привести к тому, что наш собственный проект сломается. Одним из возможных решений проблемы является локализация обращений к чужому коду при помощи шаблона проектирования «адаптер» [1]. Адаптер покрывается тестами и, в случае изменения стороннего кода, мы быстро узнаем что именно сломалось; для формализации требований заказчика (тесты заказчика, приемочные тесты). Одним из принципов, модного в настоящее время, экстремального программирования является тесное взаимодействие с заказчиком. Заказчик, так или иначе, формулирует свои требования, которые можно выразить в виде наборов тестов.

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


TDD - Разработка через тестирования. Урок 1. Введение.Основы TDD и Unit Тестирования.

2 Организация тестирования

Тесты (test cases) могут группироваться в наборы (test suites). В случае провала тестирования, программист увидит имена виновников — наборов и тестов. В связи с этим, имя должно сообщать программисту о причинах неприятности и вовлеченных модулях.

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

Обычно имя теста состоит из наименования тестируемого класса или функции и отражает особенности проверяемого поведения. Например, имена TestIncorrectLogin и TestWeakPasswordRegistration могли бы использоваться для проверки корректности логина и реакции модуля на слишком простой пароль.

Тесты могут располагаться как вместе с тестируемым кодом, так и в отдельном проекте. Очевидно, что основной проект не должен зависеть от тестов, ему незачем знать что его кто-то тестирует. Разделение тестов и рабочего кода считается хорошим тоном, однако бывают исключения — например, если нам надо проверить реализацию, т.е. корректность работы его закрытых (private) методов класса. В таком случае тесты могут размещаться прямо внутри тестируемого кода (или используется отношение дружбы между тестируемым классом и тестом). Обычно такой необходимости не возникает, а тестами покрывают лишь интерфейс, т.к. именно он формирует поведение класса.

Мало того, что основной проект не должен зависеть от тестов — тесты не должны зависеть друг от друга. В противном случае на результаты тестирования может оказывать влияние порядок выполнения тестов (т.е. фаза луны). Из этого простого правила можно сделать следующие важные выводы:

недопустимо изменять глобальные объекты в тестах и тестируемых модулях. Если же тестируемый код взаимодействует с глобальным объектом (например базой данных), то проверка корректности такого взаимодействия — удел системного тестирования; каждый тестовый случай должен быть настроен на выявление лишь одной возможной ошибки — иначе по имени ошибки нельзя установить причину неполадок; модули проекта должны соблюдать принцип единой ответственности ( Single responsibility principle, SRP) — в противном случае наши тесты будут выявлять более чем по одной ошибке ; тесты не должны дублировать друг друга. Методология разработки через тестирование (test driven developing, TDD) предполагает короткие итерации, каждая из которых содержит этап рефакторинга (переработки кода). Переработке должен подвергаться не только код основного проекта, но и тесты. Рис. 1 Модель Test Driven Developing

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

3 Использование boost test framework

Каждый файл, использующий boost test framework, должен подключать соответствующий заголовочный файл:

#include <boost/test/unit_test.hpp>

Как известно, любая программа должна иметь точку входа (функцию, с которой начинается выполнение программы). В зависимости от того, как организован проект, вы можете либо написать эту функцию сами, либо доверить всю работу Boost. Функция main будет добавлена автоматически, если перед подключением unit_test.hpp объявлены константы BOOST_TEST_MAIN и BOOST_TEST_DYN_LINK. Вызовы всех тестов проекта будут автоматически помещены в сгенерированную функцию.

Тестовые случаи группируются в наборы посредством макросов BOOST_AUTO_TEST_SUITE и BOOST_AUTO_TEST_SUITE_END, обозначающих начало и конец набора соответственно. Если тест описан вне набора, то он автоматически попадает в главный test suite. По умолчанию главному набору присвоено имя «Master Test Suite», но можно задать другое — в константе BOOST_TEST_MODULE.

При запуске тестирования программа будет последовательно входить в наборы и выполнять вложенные тесты. Чтобы проследить этот процесс по шагам — можно передать исполняемому файлу аргумент —log_level=test_suite (он используется в первом примере статьи).

Для описания тестовых случаев применяется макрос BOOST_AUTO_TEST_CASE, содержащий имя и код теста. Код теста содержит специальные макросы, проверяющие соответствие фактических результатов работы функции ожидаемым. Макросы отличаются уровнем предупреждения (CHECK — ошибка, REQUIRE — критическая ошибка, WARN — предупреждение). Среди макросов есть следующие:

BOOST_CHECK(условие) — сообщает об ошибке, если условие ложно; BOOST_REQUIRE_EQUAL( аргумент_1 , аргумент_2 ) — сообщает о критической ошибке, если аргумент_1 не равен аргумент_2 ; BOOST_WARN_MESSAGE(условие, сообщение) — выводит предупреждение с текстом сообщения, если условие ложно; BOOST_CHECK_NO_THROW(выражение) — сообщает об ошибке, если при вычислении выражения вырабатывается исключение; BOOST_CHECK_THROW(выражение, исключение) — сообщает об ошибке, если при вычислении выражения не вырабатывается исключение требуемого типа; BOOST_CHECK_CLOSE_FRACTION( аргумент_1 , аргумент_2 , погрешность ) — проваливает тест если аргумент_1 не равен аргумент_2 с заданной погрешностью.

Более полный список доступных макросов можно найти в официальной документации [2].

4 Простой пример использования boost test framework

Допустим, нам нужна функция, выполняющая нечеткое сравнение двух чисел (сравнение с допустимой погрешностью). Согласно принципам TDD, разработка начинается с описания тестовых случаев и «заглушки» функции, которая заваливает тесты (рис. 1).

#define BOOST_TEST_MAIN #define BOOST_TEST_DYN_LINK #include <boost/test/unit_test.hpp> namespace compare_nsp { inline bool is_close(const float a, const float b, const float epsilon = 0.00001) { throw "notImplemented"; } } #include "../../utils/fuzzycompare.h" #include <boost/test/unit_test.hpp> BOOST_AUTO_TEST_SUITE(TestFuzzyCompare) BOOST_AUTO_TEST_CASE(Equal) { const float d = 0.0001, dd = 0.00001, a = 3, b = a + d - dd; BOOST_REQUIRE(compare_nsp::is_close(a, b, d)); } BOOST_AUTO_TEST_CASE(NotEqual) { const float d = 0.0001, dd = 0.00001, a = 3, b = a + d + dd; BOOST_REQUIRE_EQUAL(compare_nsp::is_close(a, b, d), false); } BOOST_AUTO_TEST_SUITE_END()

Внутри набора с именем TestFuzzyCompare описаны два тестовых случая. В первом — аргументы функции эквивалентны с заданной погрешностью, поэтому в качестве результата ожидается true и используется макрос BOOST_REQUIRE. Во втором случае — числа не равны, используется макрос BOOST_REQUIRE_EQUAL(выражение, false).

Запуск проекта с опцией —log_level=test_suite выведет информацию о порядке и результатах прохождения всех описанных нами тестов. Обычно порядок прохождения тестов не интересен, поэтому дальше мы будем обходиться без этой опции.

Running 2 test cases…

Entering test suite «Master Test Suite»

Entering test suite «TestFuzzyCompare»

Entering test case «Equal»

unknown location(0): fatal error in «Equal»: C string: notImplemented

../../../boost-test/tests/utils/test_fuzzycompare.cpp(10): last checkpoint

Leaving test case «Equal»

Entering test case «NotEqual»

unknown location(0): fatal error in «NotEqual»: C string: notImplemented

../../../boost-test/tests/utils/test_fuzzycompare.cpp(17): last checkpoint

Leaving test case «NotEqual»

Leaving test suite «TestFuzzyCompare»

Leaving test suite «Master Test Suite»

*** 2 failures detected in test suite «Master Test Suite»

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

#include <cmath> namespace compare_nsp { inline bool is_close(const float a, const float b, const float epsilon = 0.00001) { return std::fabs(a - b) < epsilon; } }

Теперь программа пройдет все тесты без ошибок.

Running 2 test cases…

*** No errors detected

Описанная функция используется в более сложном примере, описанном дальше — весь исходный код упакован в один архив (см. в конец статьи).

5 Пример unit-тестирования

Предположим, что перед нами поставлена задача — разработать программу, находящую действительные корни квадратных уравнений. Заказчик сообщил нам, что уравнения должны задаваться тремя дробными числами \(a, b, c : a*x^2 + b*x + c = 0\).

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

\(

D = b^2 — 4 \cdot a \cdot c,\\

\begin{equation*}

x_{1,2} =

\begin{cases}

\frac {-b \pm \sqrt{D}}{2 \cdot a} \quad &\text{$D > 0$} \\

\frac {-b + \sqrt{D}}{2 \cdot a} \quad &\text{$D = 0$} \\

\emptyset \quad &\text{$D

\end{cases}

\end{equation*}\)

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

BOOST_AUTO_TEST_CASE(solve_quadratics_Two_Root) { std::vector<double> solution; const unsigned int nSolution = 2; const double good_solution[nSolution] = { -0.5, -3 }; BOOST_CHECK_NO_THROW(solution = solve_quadratics(2, 7, 3)); BOOST_REQUIRE(solution.size() == nSolution); BOOST_REQUIRE_CLOSE(solution[0], good_solution[0], Accuracy); BOOST_REQUIRE_CLOSE(solution[1], good_solution[1], Accuracy); } BOOST_AUTO_TEST_CASE(solve_quadratics_Single_root) { std::vector<double> solution; const unsigned int nSolution = 1; const double good_solution = -1.; BOOST_CHECK_NO_THROW(solution = solve_quadratics(4, 8, 4)); BOOST_REQUIRE(solution.size() == nSolution); BOOST_REQUIRE_CLOSE(solution[0], good_solution, Accuracy); } BOOST_AUTO_TEST_CASE(solve_quadratics_No_root) { const unsigned int nSolution = 0; std::vector<double> solution; BOOST_CHECK_NO_THROW(solution = solve_quadratics(1, 2, 3)); BOOST_REQUIRE(solution.size() == nSolution); } double get_discriminant(const double a, const double b, const double c) { return b*b - 4*a*c; } std::vector<double> solve_quadratics(const double a, const double b, const double c) { double discriminant = get_discriminant(a, b, c); if (discriminant < 0) return std::vector<double>(); double root_1 = (-b + std::sqrt(discriminant)) / (2*a), root_2 = (-b - std::sqrt(discriminant)) / (2*a); if (compare_nsp::is_close(discriminant, 0)) return std::vector<double>({root_1}); return std::vector<double>({root_1, root_2}); }

Мы отмечали правило, заключающееся в том, что на один тест должна приходиться одна проверка. Под проверкой имеется ввиду случай, описанный заказчиком, а не проверка условия. Значит наши тесты не нарушают правило, хотя и содержат по несколько проверяющих макросов.

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

Наконец, после недолгого использования, заказчик мог бы найти ошибки в нашей программе и написать дополнительные требования:

\(

\begin{equation*}

x_{1,2} =

\begin{cases}

\frac {-c}{b} \quad &\text{$a = 0, b \ne 0$} \\

NoEquationException \quad &\text{$a = 0, b = 0$}

\end{cases}

\end{equation*}\)

При параметре a равном нулю уравнение становится линейным и имеет лишь один корень. Если же и a и b равны нулю — программа может как не иметь корней, так и иметь их бесконечно много. Пусть наш код вырабатывает исключение в этом случае.

BOOST_AUTO_TEST_CASE(solve_quadratics_Linear_equation) { std::vector<double> solution; const unsigned int nSolution = 1; const double good_solution = -2./3.; BOOST_CHECK_NO_THROW(solution = solve_quadratics(0, 3, 2)); BOOST_REQUIRE_EQUAL(solution.size(), nSolution); BOOST_REQUIRE_CLOSE(solution[0], good_solution, Accuracy); } BOOST_AUTO_TEST_CASE(solve_quadratics_linear_NoEquation) { BOOST_REQUIRE_THROW(solve_quadratics(0, 0, 5), NoEquationException); }

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

class NoEquationException : public std::exception { public: const char* what() const throw (); }; std::vector<double> solve_quadratics(const double a, const double b, const double c); //!< возвращает список корней уравнения a*(x^2) + b*x + c = 0 double solve_linear(const double b, const double c); //!< возвращает корень уравнения b*x + c = 0; double get_discriminant(const double a, const double b, const double c); //!< вычисление дискриминанта уравнения a*(x^2) + b*x + c = 0 } const char* NoEquationException::what() const throw () { return "argument is not an equation"; } double get_discriminant(const double a, const double b, const double c) { return b*b - 4*a*c; } double solve_linear(const double b, const double c) { if (compare_nsp::is_close(b, 0)) throw NoEquationException(); return -c / b; } std::vector<double> solve_quadratics(const double a, const double b, const double c) { if (compare_nsp::is_close(a, 0)) return std::vector<double>({solve_linear(b, c)}); double discriminant = get_discriminant(a, b, c); if (discriminant < 0) return std::vector<double>(); double root_1 = (-b + std::sqrt(discriminant)) / (2*a), root_2 = (-b - std::sqrt(discriminant)) / (2*a); if (compare_nsp::is_close(discriminant, 0)) return std::vector<double>({root_1}); return std::vector<double>({root_1, root_2}); }

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

Исходый код проекта

Литература по теме статьи:

Васильев В.С. Паттерн «Адаптер» [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/1372. Дата обращения: 24.11.2014. Документация Boost Test Framework [Электронный ресурс] – режим доступа: http://www.boost.org/doc/libs/1_36_0/libs/test/doc/html/utf/testing-tools/reference.html. Дата обращения: 24.11.2014.
rss