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

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

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

 

Преимущества юнит-тестирования

Помимо того, что unit-тесты дают программисту уверенность при рефакторинге кода и расширении его функциональности, есть еще ряд плюсов, о которых стоит рассказать. Прежде всего, это отделение интерфейса от реализации. Очень часто одни классы используют функции других. Это абсолютно нормально, но в модульном тестировании такое недопустимо. Если мы проверяем работу какого-либо класса, то эта проверка не должна распространяться на другие. Например, если тестируемый класс пользуется базой данных, то в unit-test мы должны абстрагироваться от нее, заменив БД заглушкой. Такой подход приводит к менее связному коду и минимизирует зависимости в системе, что является несомненным преимуществом — ошибка в одном месте программы не приводит к багам в другом.

Также unit-тесты можно использовать как «живую» документацию к существующему коду. Проще говоря, в качестве примеров. Программисту, который в дальнейшем будет сопровождать наш код, не составит особого труда разобраться во всем хитроумном плане (к тому же, через полгода-год можно и самому забыть, как же эта штука должна работать и что с ней надо делать).

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

 

Немного тонкостей

В идеальном случае юнит-тесты следует писать на этапе проектирования того или иного модуля. То есть, сначала мы определяем функциональность класса/модуля, затем пишем тесты под него и только потом основной код. Время на разработку в этом случае увеличивается, но зато значительно повышается эффективность. Это называется «разработка через тестирование».

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

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

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

 

CppUnit

Наверное, один из самых известных unit-test фреймворков для C++. Он основан на JUnit — библиотеке для модульного тестирования под Java. Несмотря на свою высокую популярность, CppUnit является достаточно сложной системой, и чтобы начать работать, придется прочитать изрядное количество документации.
Давай попробуем немного разобраться с ним. Для написания простейшего теста нам понадобится класс TestCase, который будет служить базовым для уже реального тестового класса. Унаследовав TestCase, мы должны переопределить метод runTest(), который и будет выполнять основную работу.

 

Использование CppUnit::TestCase

class ComplexNumberTest : public CppUnit::TestCase
{
public:
ComplexNumberTest( std::string name ) :
CppUnit::TestCase( name )
{
}
void runTest()
{
CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );
CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );
}
};

Вроде бы все просто, но если мы хотим выполнить много разных маленьких тестов для одного и того же набора данных, то нам понадобится класс TestFixture в связке с TestCaller. TestFixture позволяет задать набор начальных данных, на которых будут производиться тесты. Делается это с помощью переопределения функции-члена setUp(). В случае, если нам понадобится убрать за собой (например, освободив выделенную память), можно воспользоваться методом tearDown(), в коде которого нам нужно будет выполнить необходимые действия. Для самих тестов следует написать свои методы. Их может быть сколько угодно, и названия этих функций произвольны.

 

Использование CppUnit::TestFixture

class Complex
{
friend bool operator ==(const Complex& a, const Complex& b);
double real, imaginary;
public:
Complex( double r, double i = 0 )
real(r),
imaginary(i)
{
}
};
bool operator ==( const Complex &a, const Complex &b )
{
return a.real == b.real && a.imaginary == b.imaginary;
}
class ComplexNumberTest :
public CppUnit::TestFixture
{
private:
Complex *m_10_1, *m_1_1, *m_11_2;
public:
void setUp()
{
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}
void tearDown()
{
delete m_10_1;
delete m_1_1;
delete m_11_2;
}
};

Чтобы запустить тесты на выполнение, нам понадобится класс TestCaller. Это шаблонный класс, созданный в примере выше, который использует наследников TestFixture. Конструктор TestCaller принимает в качестве аргументов два параметра, один из которых — это имя теста, а второй — указатель на метод, выполняющий непосредственные проверки. Для наглядности немного кода:

 

Использование CppUnit::TestCaller

class ComplexNumberTest :
public CppUnit::TestFixture
{
...
public:
...
void testEquality()
{
CPPUNIT_ASSERT( m_10_1 == *m_10_1 );
CPPUNIT_ASSERT( !(
m_10_1 == *m_11_2) );
}
void testAddition()
{
CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
}
};
CppUnit::TestCaller<ComplexNumberTest> test(
"testEquality", &ComplexNumberTest::testEquality
);
CppUnit::TestResult result;
test.run( &result );

То, что мы сейчас сделали, в терминологии CppUnit называется Test Case. Использовать Test Case в качестве основного механизма вызова тестов — не очень хорошее решение. Во-первых, мы не увидим никакой информации о том, как протекает тестирование, а во-вторых, TestCaller работает только с одним из тестов. Но тест-кейсы можно объединить в Suite с помощью класса CppUnit::TestSuite. Для этого у него имеется специальный метод addTest, принимающий в качестве параметра указатель на объект типа TestCaller.

 

Использование CppUnit::TestSuite

CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testEquality",
&ComplexNumberTest::testEquality ) );
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testAddition",
&ComplexNumberTest::testAddition ) );
suite.run( &result );

В свою очередь, запустить все наборы тестов (Test Suite) поможет класс TestRunner. С помощью метода addTest мы добавляем в ранер нужные нам сьюты. Но добавляем не просто так, а с помощью статического метода suite, который возвращает указатель на объект TestSuite.

 

Использование CppUnit::TestRunner

class ComplexNumberTest :
public CppUnit::TestFixture {
...
public:
static CppUnit::Test *suite()
{
CppUnit::TestSuite *suiteOfTests =
new CppUnit::TestSuite( "ComplexNumberTest" );
suiteOfTests->addTest(
new CppUnit::TestCaller<ComplexNumberTest>(
"testEquality",
&ComplexNumberTest::testEquality ) );
suiteOfTests->addTest(
new CppUnit::TestCaller<ComplexNumberTest>(
"testAddition",
&ComplexNumberTest::testAddition ) );
return suiteOfTests;
}
...
};
int main( int argc, char **argv)
{
CppUnit::TextUi::TestRunner runner;
runner.addTest( ExampleTestCase::suite() );
runner.addTest( ComplexNumberTest::suite() );
runner.run();
return 0;
}

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

 

Google C++ Testing Framework

Еще один популярный фреймворк для модульного тестирования — Google C++ Testing Framework. Гугл выложил его в публичный доступ под BSD-лицензией в середине 2008 года, и с тех пор он достаточно быстро набрал армию поклонников. Google Test (будем называть его так для краткости) основан на методологии xUnit, что делает его очень похожим на CppUnit. Но, в отличие от последнего, гугловские тесты проще в использовании, а потому позволяют сосредоточиться на разработке кода тестовых функций, а не инфраструктуры для их использования.

Как и в CppUnit, минимальной единицей тестирования является одиночный тест. Каждый тест основан на утверждении в виде макроса — например, ASSERT_TRUE или EXPECT_GE. Утверждения бывают фатальные и не фатальные. Фатальные макросы вида ASSERT_xxx приводят к остановке процесса тестирования, а нефатальные (или мягкие) утверждения вида EXPECT_xxx позволяют довести тесты до конца, просто выводя информацию об ошибке.

Для создания одного элементарного теста нам понадобится макрос TEST. Первым его параметром является имя набора теста, а вторым — имя теста. Тесты, схожие по смыслу, должны группироваться в наборы. Для примера взглянем на код:

 

Использование макроса TEST()

int Factorial(int n); // Считает факториал n
// Проверить факториал от 0.
TEST(FactorialTest, HandlesZeroInput)
{
EXPECT_EQ(1, Factorial(0));
}
// Проверить факториал некоторых положительных значений.
TEST(FactorialTest, HandlesPositiveInput)
{
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}

В Google Test также имеются и fixture, то есть классы для использования единой конфигурации в нескольких тестах. Такие классы являются потомками ::testing::Test, у которого имеются методы SetUp и TearDown для инициализации и освобождения ресурсов соответственно. Это очень похоже на то, что мы делали в TestFixture в CppUnit.

 

Использование макроса ::testing::Test

template <typename E> // E — тип элемента
class Queue
{
public:
Queue();
void Enqueue(const E& element);
// Возвращает NULL, если очередь пуста
E* Dequeue();
size_t size() const;
...
};
// Определяем тестовый класс
class QueueTest :
public ::testing::Test
{
protected:
virtual void SetUp()
{
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// virtual void TearDown() {}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};

Для создания теста, использующего класс набора данных, нам пригодится макрос TEST_F(). Аргументы, передаваемые TEST_F, аналогичны тем, что используются в TEST(), за единственным исключением — имя набора теста должно совпадать с именем тестового класса. Главное тут не перепутать TEST с TEST_F, иначе мы получим ошибки на этапе компиляции.

 

Использование макроса TEST_F()

TEST_F(QueueTest, IsEmptyInitially)
{
EXPECT_EQ(0, q0_.size());
}
TEST_F(QueueTest, DequeueWorks)
{
int* n = q0_.Dequeue();
EXPECT_EQ(NULL, n);
n = q1_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(1, *n);
EXPECT_EQ(0, q1_.size());
delete n;
...
}

Ну и, наконец, для запуска всех описанных тестов можно воспользоваться макросом RUN_ALL_TESTS(). Простота Google Test заключается в том, что не надо никаких дополнительных движений по добавлению функций в какие-либо контейнеры и прочее. Все, что описано с помощью макросов, выполнится при вызове RUN_ALL_TESTS.

 

Использование макроса RUN_ALL_TESTS()

int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

Самые ленивые могут не писать свою собственную функцию main, а воспользоваться готовой из гугловского набора. Для этого достаточно лишь прилинковать библиотеку gtest_main.

 

Заключение

Набор фреймворков для юнит-тестов не ограничивается приведенным мини-списком. Есть еще Boost Test, CxxTest, API Sanity Autotest для динамических C/C++ библиотек в Unix-подобных ОС и множество других. Описать все их особенности в одной статье просто невозможно. Главное, что у того, кого по-настоящему заинтересует Unit Tests или разработка через тестирование, всегда будет нормальный выбор.

 

Ссылки

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии