Личный опыт разработки ПО

Сборник рецептов

Boost Test, юнит-тестирование и CMake

комментариев 10

Boost Test

Написанием модульных тестов можно не только повысить скорость разработки за счет экономии времени на отладке, но и повысить качество. Также написание тестов позволяет критично взглянуть на интерфейсы классов и функций, что выливается в создание простых и логичных интерфейсов. Но разработка с применением тестов может не принести ощутимых плодов из-за сложности написания тестов, что выльется в слабое покрытие кода тестами. Поэтому инструмент для тестирования должен быть максимально простым, написание тестов должно происходить с приложением минимального количества усилий. Я пользовался фреймворком для написания тестов UnitTest++ – это очень хороший и удобный инструмент и если вы не используете Boost, я бы порекомендовал обратить на него пристальное внимание. Но в данной заметке речь пойдет не о нем, а о фреймворке Boost Test.


Введение

Под юнит-тестом подразумевается некоторое отдельное приложение при запуске которого происходит выполнение набора тестов и возвращается результат, из которого становится понятно были ли выполнены тесты без ошибок или нет. Юнит-тест может состоять из нескольких файлов с кодом – физическая организация, нескольких наборов (suite) тестов (case) – логическая организация. Тест как правило представляет собой просто несколько утверждений, нарушение которых означает ошибку. Результаты могут выводится как в читаемом человеком виде, так и в виде XML. Из выводимых сообщений можно узнать в каком месте теста произошло нарушение условий.

Рассмотрим простой класс, единственная задача которого – деление и умножение некоторого числа и сохранение результата:

class Calculator
{
public:
	explicit Calculator(int value)
		: Value_(value)
	{
	}
 
	void Divide(int value)
	{
		if (value == 0)
		{
			throw std::invalid_argument("Деление на ноль!");
		}
		Value_ /= value;
	}
 
	void Multiply(int value)
	{
		Value_ *= value;
	}
 
	int Result() const
	{
		return Value_;
	}
 
private:
	int Value_;
};

Тестировать будем следующим образом – сначала вызовем метод Divide с несколькими значениями и сравним результат возвращаемый Result с эталонными значениями, потом проделаем это с методом Multiply. Вот псевдокод:

Создать экземпляр Calculator со значением 12
Если Result не равен 12 вернуть ошибку
Вызвать метод Divide со значением 3
Если Result не равен 4 вернуть ошибку
Вызвать метод Divide со значением 2
Если Result не равен 2 вернуть ошибку
Вызвать метод Multiply со значением 2
Если Result не равен 4 вернуть ошибку
Вызвать метод Multiply со значением 3
Если Result не равен 12 вернуть ошибку

А вот C++ и фреймворк Boost Test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "calculator.h"
 
#include <boost/test/unit_test.hpp>
 
#define BOOST_TEST_MODULE testCalculator
 
BOOST_AUTO_TEST_CASE(testCalculator)
{
	Calculator calculator(12);
	BOOST_CHECK_EQUAL(calculator.Result(), 12);
	calculator.Divide(3);
	BOOST_CHECK_EQUAL(calculator.Result(), 4);
	calculator.Divide(2);
	BOOST_CHECK_EQUAL(calculator.Result(), 2);
	calculator.Multiply(2);
	BOOST_CHECK_EQUAL(calculator.Result(), 4);
	calculator.Multiply(3);
	BOOST_CHECK_EQUAL(calculator.Result(), 12);
}

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

  1. В третьей строке подключается заголовочный файл Boost Test содержащий необходимые макросы. Без этого понятно, работать ничего не будет.
  2. Пятая строка тоже необходима, в ней определяется имя модульного теста. Данная строка должна быть в одном из cpp-файлов проекта с тестами.
  3. В седьмой строке создается минимальная единица теста содержащая ряд условий. Невыполнение одного из условий приведет к выдаче сообщения о неправильном выполнении теста.

Улучшим тест. Например сейчас у нас в одном месте тестируется два метода, что не очень правильно – пусть будет по тесту на каждый метод:

#include "calculator.h"
 
#include <boost/test/unit_test.hpp>
 
#define BOOST_TEST_MODULE testCalculator
 
BOOST_AUTO_TEST_CASE(testCalculator)
{
	Calculator calculator(12);
	BOOST_CHECK_EQUAL(calculator.Result(), 12);
}
 
BOOST_AUTO_TEST_CASE(testCalculatorDivide)
{
	Calculator calculator(12);
	calculator.Divide(3);
	BOOST_CHECK_EQUAL(calculator.Result(), 4);
	calculator.Divide(2);
	BOOST_CHECK_EQUAL(calculator.Result(), 2);
}
 
BOOST_AUTO_TEST_CASE(testCalculatorMultiply)
{
	Calculator calculator(12);
	calculator.Multiply(2);
	BOOST_CHECK_EQUAL(calculator.Result(), 24);
	calculator.Multiply(3);
	BOOST_CHECK_EQUAL(calculator.Result(), 72);
}

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

#include "calculator.h"
 
#include <boost/test/unit_test.hpp>
 
#define BOOST_TEST_MODULE testCalculator
 
struct Fixture
{
	Fixture()
		: calculator(12)
	{
		// Здесь тестовый объект можно настроить
	}
 
	~Fixture()
	{
		// А здесь корректно завершить с ним работу
	}
 
	// А вот и сам тестовый объект
	Calculator calculator;
};
 
BOOST_FIXTURE_TEST_CASE(testCalculator, Fixture)
{
	BOOST_CHECK_EQUAL(calculator.Result(), 12);
}
 
BOOST_FIXTURE_TEST_CASE(testCalculatorDivide, Fixture)
{
	calculator.Divide(3);
	BOOST_CHECK_EQUAL(calculator.Result(), 4);
	calculator.Divide(2);
	BOOST_CHECK_EQUAL(calculator.Result(), 2);
}
 
BOOST_FIXTURE_TEST_CASE(testCalculatorMultiply, Fixture)
{
	calculator.Multiply(2);
	BOOST_CHECK_EQUAL(calculator.Result(), 24);
	calculator.Multiply(3);
	BOOST_CHECK_EQUAL(calculator.Result(), 72);
}

Вот теперь все красиво и правильно.

Логическая организация юнит-теста

Как уже упоминалось модульный тест может состоять из нескольких наборов тестов, наборы тестов в свою очередь могут состоять просто из тестов :) Например юнит-тест библиотеки может содержать несколько наборов тестов, по  набору на каждый тестируемый класс, и в наборе по тесту на каждый тестируемый метод. Для компоновки тестов в набор Boost Test предоставляет макрос BOOST_AUTO_TEST_SUITE. Так мы могли бы скомпоновать тесты в набор:

#include "calculator.h"
 
#include <boost/test/unit_test.hpp>
 
#define BOOST_TEST_MODULE testCalculator
 
BOOST_AUTO_TEST_SUITE(testSuiteCalculator) // Начало набора тестов
 
struct Fixture
{
	...
};
 
BOOST_FIXTURE_TEST_CASE(testCalculator, Fixture)
{
	...
}
 
BOOST_FIXTURE_TEST_CASE(testCalculatorDivide, Fixture)
{
	...
}
 
BOOST_FIXTURE_TEST_CASE(testCalculatorMultiply, Fixture)
{
	...
}
 
BOOST_AUTO_TEST_SUITE_END() // Конец набора тестов

Типы проверок

Мы рассмотрели проверку равенства двух сущностей BOOST_CHECK_EQUAL, но помимо этого Boost Test предоставляет еще ряд инструментов:

BOOST_CHECK(условие)
Простейшая проверка истинно условие или нет.
BOOST_CHECK_EQUAL(значение_1, значение_2)
Проверка на равенство двух значений.
BOOST_CHECK_CLOSE(значение_1, значение_2, точность)
Проверка на равенство чисел с плавающей точкой. Два значения считаются равными, если не отличаются на значение более указанного (в процентах). Чтобы использовать данную проверку, необходимо подключить заголовочный файл

#include <boost/test/floating_point_comparison.hpp>
BOOST_CHECK_BITWISE_EQUAL(значение_1, значение_2)
Отличная штука! Проверит два значения побитово и сообщит в каком месте биты отличаются.
BOOST_CHECK_EQUAL_COLLECTIONS(начало_1, конец_1, начало_2, конец_2)
Проверка равенства двух последовательностей (массивов или контейнеров), на вход получает пару начало-конец последовательности (указатели или итераторы).
BOOST_CHECK_THROW(инструкция, исключение)
Проверка, что при выполнении инструкции будет выброшено указанное исключение. Инструкцией может быть например вызов метода. Так в приведенном примере мы должны были проверить, что метод Divide, вызванный с параметром 0 выбрасывает исключение:

BOOST_CHECK_THROW(calculator.Divide(0), std::invalid_argument);
BOOST_CHECK_NO_THROW(инструкция)
А здесь наоборот проверяется, что при выполнении инструкции исключений выброшено не будет.

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

Я привел наиболее часто встречающиеся проверки, за полным списком обращайтесь к официальному руководству на сайте Boost.

Тестирование закрытых методов класса

Рекомендуется тестировать классы только через открытый интерфейс, но иногда возникает обоснованное желание протестировать закрытый член класса. В этом случае можно вспомнить, что в C++ у классов могут быть друзья. Так чтобы сделать наши тесты друзьями класса Calculator, необходимо изменить его следующим образом:

namespace testSuiteCalculator // Обратите внимание - это имя набора тестов
{
	// А это имена конкретных тестов
	struct testCalculator;
	struct testCalculatorDivide;
	struct testCalculatorMultiply;
}
 
class Calculator
{
	friend struct ::testSuiteCalculator::testCalculator;
	friend struct ::testSuiteCalculator::testCalculatorDivide;
	friend struct ::testSuiteCalculator::testCalculatorMultiply;
 
public:
	...
};

Если наборы тестов не используются, то достаточно имен тестов в роли имен структур. Пространства имен в данном случае не используются.

Интеграция тестов в систему сборки CMake

А теперь самое вкусное – простая сборка и добавление тестов в проект. Поможет нам в этом CMake. Итак порядок добавления тестов:

  1. Создается директория test в корне проекта
  2. В этой директории создаются cpp-файлы с тестами (не забываем, что один из них должен содержать определение BOOST_TEST_MODULE)
  3. В CMakeLists.txt проекта добавляется следующий код:
    set (TESTS_SOURCES
    	../tests/файлы_с_тестами.cpp)
    find_package (Boost COMPONENTS unit_test_framework REQUIRED)
    include_directories(${Boost_INCLUDE_DIRS})
    set (TEST test_${PROJECT})
    add_executable (${TEST} ${TESTS_SOURCES})
    target_link_libraries (${TEST} ${PROJECT} ${Boost_LIBRARIES})
    enable_testing ()
    add_test (${TEST} ${TEST})
  4. Если проект на самом деле представляет подпроект более крупного проекта, то ничего не делаем, в противном случае добавляем в конец CMakeLists.txt главного проекта:
    enable_testing ()

Выполнив эти действия, вы после сборки проекта, сможете выполнить все тесты запустив программу ctest близкого родственника CMake. Список тестов для CTest, будут подготовлены CMake автоматически. Это очень удобно еще и тем, что в связке с этими инструментами можно использовать веб-приложение CDash. Это позволит разработчикам просматривать в своем браузере статистику по проекту. Выглядеть это может например так: CMake Dashboard

Также тесты можно запускать выполнив

make test

Или в своей IDE собрав цель RUN_TESTS.

Скачать файл с примером проекта использующего Boost Test

24th Январь 2010
19:09

10 комментариев к 'Boost Test, юнит-тестирование и CMake'

Подписаться на комментарии по RSS или TrackBack.

  1. Спасибо! Как раз то, что нужно!

    nazavrik

    13 февраля 10 15:28

  2. Рад, что информация оказалась полезной. Оставайтесь с нами :)

  3. Boost.Test был бы хорош, если бы не было Google Test.

    Начинал именно с Boost.Test, потом добавил Google Mock, чтобы писать адекватные тесты с использованием тестовых двойников ( http://xunitpatterns.com/Test%20Double.html ).

    Так бы все и оставил, если бы не:

    — практически отсутствие поддержки библиотеки (пиши тикеты — не пиши — глухо; документация не совсем актуальная и не полная; ни разу не видел, чтобы Boost.Test была в списке обновленных библиотек какой-либо версии boost — если обновляется, то в тихую и без логов);
    — присутствие конкретных ошибок (CHECK_EQUAL_COLLECTIONS не работает с std::map; CHECK_NO_THROW игнорирует зарегистрированные обработчики исключений; при желании, если поискать можно еще найти) и отсутствие элементарной функциональности, которая есть в Google Test (например, в Boost.Test можно запускать тесты по имени, но нет конструкции ‘запускать кроме’)

    Вывод

    Можно было бы удовлетвориться Boost.Test + Google Mock вполне, !если бы библиотека развивалась (а развивать там много что нужно и смысл есть, но не кому)!

    Реально нужно выбирать Google Test — документация своеобразная, но зато activity: high и поддержка хорошая (написал в googlegroups о ошибке сборки с MinGW на win32 — в течении суток ответили и тикет завели, хотя этот компилятор официально не поддерживается).

    Iakov Minochkin

    30 марта 11 19:29

  4. Спасибо, очень интересно!

  5. Огромный респект за такую статью! Ничего лишнего и все по делу!
    Остается заметить что автору следует включить раздел по автоматизации.

    Dmitry

    18 апреля 12 14:07

  6. Крутая статья!! «fdsgdfg»

    аыва "fdsgdfg"

    10 июня 12 4:56

  7. Очень интересный стиль изложения, в начале, очень интересно и красочно рассказывается про то, какая же замечательная библиотека — Boost.Test, а потом говорится, что Google Test всё-таки лучше ))

    Алексей

    25 июля 12 11:18

  8. Это одна из информационных жемчужин по тематике С/C++ тестирования :)
    Но статью нужно было начинать с конца, т.е. сначала рассказать о подъеме инфраструктуры (cmake и т.п.), а потом о том как писать тесты. Потому что в инете очень много примеров с тестами элементарных классов, а вот рассказов о том, как настроить проект с тестами практически нет :(

    Илья Винокуров

    13 января 15 9:21

  9. Благодарю Вас за хорошую и полезную статью!

    Игорь

    10 февраля 16 15:04

  10. Спасибо вам за отличную статью. То что надо для новичка в boost и в boost.test в частности.

    Павел Соловьенко

    22 марта 16 11:09

Оставить комментарий