В статье про класс Thread, реализующий потоки в С++, я обещал как минимум показать, как работать с данным классом, и как максимум рассказать про блочное (unit) тестирование в целом, и про его применение для проверки работы нашего класса в частности.
Дожив до четвертого десятка и имея за спиной десяток с хвостиком, посвященный программированию, к своему огромному стыду к программированию с использованием блочного тестирования (TDD - test driven development) я приобщился только год назад. Честно могу сказать - это было для меня одним из сильнейших потрясений в профессиональной области за последнее время, и радикально поменяло некоторые фундаментальные представления о разработке софта. Как прирожденный максималист в профессии (за что часто очень нелюбим коллегами по цеху, которые руководствуются правилом “лучшее враг хорошего”), я работаю под девизом “мои программы должны быть безупречны”. А так как тут мне дали в руки такой волшебный инструмент как блочное тестирование, я стараюсь теперь его применять где только возможно. Даже порой радикально перерабатывая старые проекты.
Ладно, это лирика. Приступим к делу.
У нас есть класс Thread, расположенный в файлах thread.cpp
и thread.h
.
Напишем небольшой пример (thread_example.cpp
).
#include <iostream> #include "thread.h" // Создаем наследника от класса Thread class MyThread: public ext::Thread { public: // Инициализируем в false флаг завершения в конструкторе MyThread() : __done(false) {} virtual void Execute() { // В процессе работы потока меняем флаг завершения на истину __done = true; } // Функция, возвращающая значение флага завершение bool done() const { return __done; } private: bool __done; }; int main(int argc, char* argv[]) { // Создаем объект потока. Пока он еще не запущен. MyThread thread; // Печатаем значение флага завершения. Должно быть 0 (false) std::cout << "Thread status before: " << thread.done() << std::endl; // Запускаем поток thread.Start(); // И ждем его завершения thread.Join(); // Если поток нормально был запущен и отработал, то значение // флага должно измениться на 1 (true). Это должна сделать // функция Execute(). Если тут будет не 1, а 0, значит поток // не выполнялся, и выходит, что с классом что-то не так. std::cout << "Thread status after: " << thread.done() << std::endl; }
Компилируем (естественно, из командной строки).
Visual Studio 2008 (хотя подойдет любая версия VS):
cl /EHsc /I. /Fethread_example /DWIN32 thread_example.cpp thread.cpp
Опция /EHsc
нужна, так как мы пишем на С++, и поэтому компилятору cl.exe
надо явно указать необходимость включения поддержки исключений. Особенность данного компилятора.
Если вы в UNIX’e, тогда, например, gcc
:
g++ -o thread_example thread_example.cpp thread.cpp
Запускаем thread_example, и имеем на экране следующее:
Thread status before: 0
Thread status after: 1
Судя по напечатанным данным, класс работает правильно.
Я специально не использовал в функции Execute()
отладочной печати на экран типа “Hello, world! I’m the thread”. Хотя это было бы нагляднее и прикольнее, чем какие-то булевы флаги. Но на это была причина. При работе с потоками, когда ваш код теперь уже выполняется нелинейно, а какие-то фрагменты могут работать параллельно, приходится очень тщательно продумывать совместное использование переменных одновременно работающими потоками. Может так случиться, что когда основной поток будет печатать что-то на экран через переменную std::cout
, параллельный поток тоже захочет это сделать, прервет основной поток на полпути и сам начнет использовать std::cout
. Данные обоих потоков смешаются, и в лучшем случае на экран вылезет каша, а в худшем программа может завершиться аварийно. На том же мной так любимом AIX’е именно это и происходит. Видимо, стандартная библиотека AIX’а требует каких-то дополнительных настроек для нормальной работы в мультипотоковой среде. Для избежания подобных проблем совместного доступа применяются различные механизмы из мира параллельного программирования - блокировки (mutex), семафоры, критические секции и т.д. Я посвящу отдельный пост этому очень непростому вопросу, но расскажу о нем крайне просто и понятно.
Теперь давайте запустим десяток потоков (thread_example2.cpp
).
#include <vector> #include <iostream> #include "thread.h" class MyThread: public ext::Thread { public: MyThread(int id) : __id(id), __done(false) {} virtual void Execute() { // Небольшая "перчинка" программы, чтобы не было скучно. // Суть в том, что поток с индексом 3 (по счету номер 4, так первый // индекс 0) не будет устанавливать флаг выполнения. Сделано это // просто для разнообразия. Результат данной "перчинки" будет виден // при печати. if (__id != 3) __done = true; } bool done() const { return __done; } private: int __id; bool __done; }; typedef std::vector<MyThread*> Threads; int main(int argc, char* argv[]) { // Создаем вектор из указателей на потоки std::vector<MyThread*> threads; // Создаем 10 потоков и сохраняем указатели на них в вектор for (int i = 0; i < 10; i++) threads.push_back(new MyThread(i)); // Запускаем потоки на выполнение for (Threads::iterator i = threads.begin(); i != threads.end(); i++) (*i)->Start(); // Дожидаемся, пока они все завершатся for (Threads::iterator i = threads.begin(); i != threads.end(); i++) (*i)->Join(); // Печатаем статусы потоков в одну строку через пробел for (Threads::iterator i = threads.begin(); i != threads.end(); i++) std::cout << (*i)->done() << " "; std::cout << std::endl; // Чистим за собой память. for (Threads::iterator i = threads.begin(); i != threads.end(); i++) delete *i; }
Компилируем.
Visual Studio:
cl /EHsc /I. /Fethread_example2 /DWIN32 thread_example2.cpp thread.cpp
В UNIX’e (gcc
):
g++ -o thread_example2 thread_example2.cpp thread.cpp
Запускаем thread_example2
, и имеем на экране следующее:
1 1 1 0 1 1 1 1 1 1
Видно, что все потоки, кроме четвертого (индекс 3, так как считаем от нуля) установили свои флаги правильно. Четвертому помешала “перчинка” (см. выше).
Что дальше? Да ничего, собственно. Теперь вы наверняка набросаете несколько своих примеров, поиграетесь, и может начнете включать данный класс в свои проекты. Тестовые примеры вы скорее всего сотрете как отработанный материал, а может и заначите до лучших времен.
А теперь! На сцену приглашается unit тестирование.
Я вам предлагаю сделать небольшие программы-тесты, которые бы своими результатами доказывали правильность работы нашего класса. Например:
class SimpleThread: public ext::Thread { public: SimpleThread() : __done(false) {} virtual void Execute() { __done = true; } bool done() const { return __done; } private: bool __done; };
Класс SimpleThread
очень похож на класс MyThread
из наших примеров выше. Он просто меняет флаг активности с false
на true
в процессе успешного выполнения.
// Декларируем тест с именем RunningInParallel в группе тестов ThreadTest. TEST(ThreadTest, RunningInParallel) { // Создаем объект нашего класса SimpleThread thread; // Внимание! Макрос EXPECT_FALSE смотрит, какое значение у его аргумента. // Если это ложь, то все нормально, и выполнение теста идет дальше. Если же нет, // то печатается сообщение об ошибке, хотя тест продолжает работу. // В нашем случае тут должно быть false по смыслу. EXPECT_FALSE(thread.done()); // Запускаем поток на выполнение thread.Start(); // Ждем завершение потока thread.Join(); // Макрос EXPECT_TRUE смотрит, какое значение у его аргумента. // Если это истина, то все нормально, и выполнение теста идет дальше. Если же нет, // то печатается сообщение об ошибке, хотя тест продолжает работу. // Тут мы уже ждем не false, а true, потому что поток должен был изменить значение // этого флага. EXPECT_TRUE(thread.done()); }
Теперь осознаем произошедшее - мы не просто написали какой-то пример, а мы формально опередили логику работы класса, задали его ответственность. Теперь наши пожелания к функциональности класса заданы не на словах и предположениях, а в виде программы.
Теперь осталось только запустить этот тест.
Существует много библиотек для unit тестирования практически для каждого языка. С++ не исключение. Самой распространенной в мире С++ является CppUnit. Но около полугода назад Google ворвался в мир библиотек тестирования с Google Test Framework. На момент написания данной статьи актуальной версией является 1.2.1. Распространяется в исходных текстах. Данную библиотеку можно прекомпилировать и использовать как двоичный модуль при линковке, но я сделал иначе. Так как я постоянно прыгаю с платформы на платформу, с компилятора на компилятор, мне удобнее компилировать Google Test прямо из исходников каждый раз при сборке проекта, благо библиотека хорошо портируема, мала по размеру и быстро компилируется. К небольшому сожалению, Google Test реализована в виде не одного файла-исходника и одного .h файла, а целого набора .h файлов и набора .cc (.cpp) файлов. Так удобно библиотеку развивать (что логично), но не использовать из исходников со стороны. Поэтому я объединил всю библиотеку в два файла: gtest.h
и gtest-all.cc
, и больше ничего не нужно. Гугловцы обещали в следующий релиз библиотеки включить мой патч на эту тему. Сейчас же они (также по моей идее) дают специальный скрипт, которым можно из официального архива сделать компактную версию из двух файлов. Для тех, у кого уже съехали мозги от этих подробностей, и кто пока не хочет тратить время на техдетали библиотеки, я могу предложить мою сборку Google Test’а. Можно начать с нее. Она основана на официальной версии 1.2.1 и является объединением множества файлов в два. В архиве два файла gtest/gtest.h
и gtest-all.cc
. Положите их в каталог, где будете проводить опыты.
Итак, предположим, вы имеете файлы: gtest/gtest.h
и gtest-all.cc
в рабочем каталоге, и все готово к запуску.
Полный вариант исходника thread_unittest.cpp
:
#include "gtest/gtest.h" #include "thread.h" class SimpleThread: public ext::Thread { public: SimpleThread() : __done(false) {} virtual void Execute() { __done = true; } bool done() const { return __done; } private: bool __done; }; TEST(ThreadTest, RunningInParallel) { SimpleThread thread; EXPECT_FALSE(thread.done()); thread.Start(); thread.Join(); EXPECT_TRUE(thread.done()); }
Я предпочитаю давать имена файлам с тестами, используя суффикс _unittest
к имени основного файла. Это позволяет, быстро взглянув на каталог, понять - какие классы имеют тесты, а какие нет.
Также нам нужен стартовый файл runner.cpp
, который будет содержать функцию main()
:
#include "gtest/gtest.h" int main(int argc, char* argv[]) { // Инициализируем библиотеку testing::InitGoogleTest(&argc, argv); // Запускаем все тесты, прилинкованные к проекту return RUN_ALL_TESTS(); }
Тут все просто. Обычно, этот файл одинаков для все тестовых проектов, если вам не надо проводить какие-нибудь дополнительные инициализации, брать что-то из командной строки и.д. Google Test устроена так (в отличие от CppUnit, например), что тесты (TEST
и TEST_F
) не надо нигде дополнительно регистрировать, объявлять и т.д. Вы просто задаете тело теста, включаете файл с исходником в проект и все. Далее все происходит автоматически.
Резонный вопрос - а в каком порядке тесты буду выполнены, если их несколько? А ответ прост: вас это не касается. Тесты могут выполняться в любом порядке, и нельзя делать никаких предположений на эту тему. Суть тут в том, что каждый тест должет быть атомарным и независимым (конечным автоматом без памяти). В этом суть блочного (unit) тестирования, когда маленькие кусочки большой программы проверяются отдельно, в полной изоляции. Но, вернемся к компиляции.
Компилируем.
Visual Studio:
cl /EHsc /DWIN32 /I. /Fethread_unittest.exe runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
UNIX:
g++ -I. -o thread_unittest runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
Запускаем thread_unittest
и получаем что-то вроде:
[==========] Running 1 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 1 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
[ OK ] ThreadTest.RunningInParallel
[----------] Global test environment tear-down
[==========] 1 tests from 1 test case ran.
[ PASSED ] 1 tests.
Это значит, что тест был запущен и отработал как положено.
Добавим еще один тест, который будет проверять, убивается ли поток, когда мы этого хотим.
thread_unittest.cpp
:
#include "gtest/gtest.h" #include "thread.h" #ifdef WIN32 #include <windows.h> #define msleep(x) Sleep(x) #else #include <unistd.h> #define msleep(x) usleep((x)*1000) #endif class SimpleThread: public ext::Thread { public: SimpleThread() : __done(false) {} virtual void Execute() { __done = true; } bool done() const { return __done; } private: bool __done; }; TEST(ThreadTest, RunningInParallel) { SimpleThread thread; EXPECT_FALSE(thread.done()); thread.Start(); thread.Join(); EXPECT_TRUE(thread.done()); } // "Нескончаемый поток" class GreedyThread: public ext::Thread { public: virtual void Execute() { // Данный поток будет работать вечно, пока его не убьют извне. while (true) { msleep(1); } } }; TEST(ThreadTest, Kill) { // Создаем "вечный" поток GreedyThread thread; // Запускаем его thread.Start(); // Убиваем его thread.Kill(); // Если функция Kill() не работает, ты мы никогда не дождемся окончания потока // и программа тут повиснет. thread.Join(); }
Компилируем.
Visual Studio:
cl /EHsc /I. /Fethread_unittest.exe /DWIN32 runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
UNIX:
g++ -I. -o thread_unittest runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
Запускаем thread_unittest
и получает что-то вроде:
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
[ OK ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 2 tests.
Оба теста отработали правильно. Получается, что теперь мы точно уверены, что наш поток умеет работать параллельно и независимо от основного потока, и умеет принудительно “убиваться” по требованию. Мы это доказали тестами, а не словами или алгоритмами на бумаге. Если вам кажется, что еще не вся функциональность класса проверена, обязательно допишите свои тесты для проверки своих предположений.
Теперь внесем в класс “случайную ошибку”, добавив оператор return
в виндовый вариант функции void Thread::Start()
:
void Thread::Start() { // "Случайная" ошибка return; __handle = CreateThread( 0, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(ThreadCallback), this, 0, 0 ); }
Теперь наш класс “сломан”. Посмотрим, что скажет тестирование (естественно, надо перекомпилировать программу перед этим):
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
thread_unittest.cpp(33): error: Value of: thread.done()
Actual: false
Expected: true
[ FAILED ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] ThreadTest.RunningInParallel
1 FAILED TEST
Бинго! Тест говорит, что ожидаемое значение флага выполнения “истина”, а реальное “ложь”. Класс не работает! Конечно не работает, так как создание потока не происходит из-за “случайного” оператора return
. Мы нашли реальный “баг”, причем сделали это автоматизированным образом.
Можно еще улучшить тест дополнительной информацией, которая будет показана в случае его сбоя:
TEST(ThreadTest, Simple) { SimpleThread thread; EXPECT_FALSE(thread.done()); thread.Start(); thread.Join(); EXPECT_TRUE(thread.done()) << "Поток не изменил флаг"; }
Теперь сообщение об ошибке будет более информативно.
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
thread_unittest.cpp(33): error: Value of: thread.done()
Actual: false
Expected: true
Поток не изменил флаг
[ FAILED ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] ThreadTest.RunningInParallel
1 FAILED TEST
Google Test имеет множество функций для тестовых сравнений, но основные их них, используемые в 99% случаев, следующие:
EXPECT_EQ(a, b)
- проверка условия “a = b”EXPECT_NE(a, b)
- проверка условия “a != b”EXPECT_GT(a, b)
- проверка условия “a > b”EXPECT_LT(a, b)
- проверка условия “a < b”EXPECT_GE(a, b)
- проверка условия “a >= b”EXPECT_LE(a, b)
- проверка условия “a <= b”EXPECT_TRUE(a)
- проверка аргумента на истинуEXPECT_FALSE(a)
- проверка аргумента на ложьФункции, начинающиеся с EXPECT_
, в случае ошибки не прерывают выполнение теста, а просто печатают сообщение об ошибке, и тестирование продолжается. Если ваша ошибка фатальна (например, база данных недоступна), и нет причин продолжать тесты вообще, то можно использовать функции со схожим именованием:
ASSERT_EQ(a, b)
- проверка условия “a = b”ASSERT_NE(a, b)
- проверка условия “a != b”ASSERT_GT(a, b)
- проверка условия “a > b”ASSERT_LT(a, b)
- проверка условия “a < b”ASSERT_GE(a, b)
- проверка условия “a >= b”ASSERT_LE(a, b)
- проверка условия “a <= b”ASSERT_TRUE(a)
- проверка аргумента на истинуASSERT_FALSE(a)
- проверка аргумента на ложьЭти фунции при ошибке прерывают тест и весь процесс тестирования с целом.
Есть еще особая функция FAIL()
, которая безусловно прерывает тест с ошибкой. Удобно для проверки мест, где вы “не должны” оказаться в процесса работы теста. Например:
try { ... } catch(...) { FAIL() << "Данный кусок программы не должен генерировать исключений"; }
Полный список функций-проверок, а также описания прочих возможностей Google Test, так как я затронул пока лишь малую их часть, можно получить в документации.
Кроме того, во все эти функции можно писать как стандартные потоки вывода через оператор <<
, как мы делали в примере выше:
EXPECT_TRUE(thread.done()) << "Поток не изменил флаг";
тем самым печатая удобную отладочную информацию.
Давайте проанализируем сказанное и сделанное. Что мы получили? Как я уже говорил, мы формализовали наши требования от класса в виде программы, которую можно теперь запускать сколько угодно раз, проверяя работу класса. Вы спросите для чего? Класс-то работает. А вот представьте, что вы установили новую версию компилятора или новую версию библиотеки pthread
и что-то в этом роде. Вы уверены, что в них нет ошибок? или может нужны другие опции командной строки для правильной работы. Кто знает?! Тест знает! Скомпилированный и запущенный тест сразу же проверит, работает ли класс так, как вы от него ожидаете. По крайне мере хуже уже не будет. Новые ошибки тест может и не покажет, но уже формализованное ранее поведение класса проверит точно. А теперь представьте, что вам надо так перепроверить сотни классов в вашем проекте. Только автоматизированное тестирование делает это реальным. А тестирование типа “давай поерзаем программой быстренько, и если сразу не сломалось, то все хорошо” тестированием не является вообще. Гораздо проще включить компилирование и запуск тестов при каждой полной сборке проекта. Небольшая потеря времени конечно есть на дополнительную компиляцию, но это с лихвой окупается выявленными тут же ошибками. Сами unit тесты обычно работают очень быстро. Они должны быть быстрыми, иначе они неудобны для регулярного запуска. Сотни тестов не должны как-либо заметно медленно работать. Если какой-то тест требует секунд для себя, то может его стоит перенести в раздел функционального тестирования и пользоваться им уже в процессе проверки программы для релиза, а не в процессе самой разработки, или запускать медленные тесты автоматически в ночных сборках.
Кстати, наличие тестов позволяет поручить возможные доработки кода не только тому, кто этот код писал изначально и понимает в самых деталях, как все работает. Если тесты работают, значит изменения кода по крайне мере не сделали его хуже, а значит клиент не будет кричать сразу после установки новой версии типа “какого вы тут все сломали”. Тесты - это прежде доказательства программиста, что его программа работает так, как он ожидает и всем обещает, как его программа должна работать. Только это уже не просто слова, а автоматизированный метод проверки.
Помните те примеры, которые мы писали в начале. Что с ними случилось? Мы их просто выкинули как отработанный материал. Выкинули результаты очень полезной работы. Мы по кусочкам разобрались, как работает исследуемый класс, но потом отказались повторно использовать уже полученные результаты, выкинув тестовые примеры. Так почему бы изначально не приложить чуть-чуть усилий и не оформить тестовые игрушечные примеры в виде блочных тестов, готовых к автоматизированному повторному использованию, и не превратить их в мощное автоматизированное оружие против багов?
Личный пример. Писал я класс, реализующий TCP/IP сокет с использованием SSL. Скачал библиотеку OpenSSL, начал разбираться. Стал писать мини примеры для освоения разных аспектов библиотеки. И каждый свой эксперимент я оформлял в виде теста (один тест для создания контекста ключей, другой для установления соединения, третий для расшифрации кодов ошибок и т.д.). Каждый новый запуск проекта влючал все больше и больше таких тестов. Затем я вынужден был прерваться на месяц. По прошествии месяца я напрочь забыл все про OpenSSL. И если бы не готовые уже тесты, я бы начал разбираться опять сначала. А так, поглядев на уже сделанные куски, я быстро погонял тесты, вспомнил что к чему, и продолжил работу. Затем из этих тестов фактически и родилась моя библиотека для работы с SSL, и сами тесты включились в тестирующую сборку. Когда осваиваешь что-то новое - язык, библиотеку и т.д. - тестовая программа очень быстро разрастается и превращается некоего монстра, в котором вы хотите задействовать и проверить все новое. Гораздо полезнее разбираться по маленьким кусочкам, изолированно изучать каждый вопрос, закрепляя полученные результаты в виде тестов.
Вы меня сходу спросите - а как писать тесты? Ведь данный пример весьма тривиален, а реальные программы гораздо сложнее, в них много взаимозависимостей, и порой крайне сложно раскроить их на тестируемые кусочки. Ответ, который я дам сходу сейчас таков - пишите ваши программы сразу пригодными для тестирования. А вот как именно это делать - я расскажу в будущих выпусках нашего научно-популярного журнала.
А вы меня опять спросите - а другие языки как? как, например, делать unit тестирование в классическом языке С? Об этом я тоже непременно расскажу.
Unit-тестирование — это громадная и очень интересная тема. Будем ее развивать.
P.S. Исходные тексты данной статьи я проверял на Windows, Linux 2.6 (32- и 64-бит Intel и SPARC), AIX 5.3 и 6, SunOS 5.2 64-bit SPARC.
Другие посты по теме: