Как писать unit тесты си
Перейти к содержимому

Как писать unit тесты си

  • автор:

Unit-тестирование в языке С

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

Мир языка С++ не такой дружественный к тестированию, как например, мир Java, C# или мир интерпретаторов. Главная причина — крайне слабый механизм интроспекции, то есть возможности исследования двоичного кода в плане получения информации о структуре исходных текстов. В Java, например, есть Reflection , с помощью которого можно прямо на основе скомпилированных классов создать тестовую среду (понять иерархию классов, типа аргументов и т.д.). В С++ приходится многое закладывать в исходный текст на этапе его создания, чтобы облегчить будущее тестирование.

А что же мы имеем в С? Тут, как мне кажется, разрыв в удобстве тестирования по отношению к С++ в разы больше, чем между С++ и Java, например. Причин море: процедурная модель вместо объектно-ориентированной, отсутствие интроспекции вообще, крайне слабая защита при работе с памятью и т.д.

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

Перебрав еще несколько вариантов, я остановился на гугловской библиотеке cmockery. Мне понравилось, что библиотека, несмотря на весьма сложный код, успешно компилируются не только в Visual Studio и GNU C, но и “родными” компиляторами AIX, HP-UX, SunOS и некоторых других экзотических зверей. Также библиотека умеет отлавливать утечки памяти, неправильную работу с распределенными кусками памяти (так называемые buffer over- и under- run). Еще в cmockery есть зачатки mock-механизмов, то есть когда задаются предполагаемые сценарии выполнения тестируемого блока, и потом результаты тестового прогона сверяются с предполагаемым сценарием. Mock-возможности я не буду пока рассматривать в данной статье. Про это стоит написать отдельно.

На текущий момент актуальной версией cmockery является 0.1.2. Из всего архива реально нужны только два файла: cmockery.c и cmockery.h . Можно, конечно, собрать библиотеку как положено, в двоичном виде, но я предпочитаю работать всегда с исходными текстами, благо компилируется очень быстро (это ж не С++).

Желающие, могут скачать мою сборку cmockery. В этом архиве только необходимые два файла cmockery.c и cmockery.h . Также в файл cmockery.h я внес небольшое изменение, связанное к тем, что функция IsDebuggerPresent() почему-то явно объявлена в заголовочных файлах только в Visual Studio 2008. Для студии 2003 и 2005 надо вручную объявлять прототип, иначе при линковке вылезает сообщение:

error LNK2019: unresolved external symbol _IsDebuggerPresent referenced in function __run_test 

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

Теперь пример реального использования cmockery .

Я долго выбирал то, на чем можно хоть как-то наглядно продемонстрировать unit-тестирование в С. В итоге я остановился на библиотеке для работы со строками. Эта библиотека реализует так называемые строки с длинной. То есть надо для кода на С дать более менее удобный интерфейс для манипулированию строками, которые хранят внутри себя длину.

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

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

Итак, библиотека cstring . Тут можно создавать в некоторые “объекты”, реализованные через структуры, которые представляют собой “строки”. Такая “строка” может создаваться либо в стеке (автоматическая переменная), либо в куче. Также предоставляется набор разнообразных базовых функций: определение длины, копирование, склейка, интерфейс со строками языка С (char *) и т.д. Как я уже сказал, для демонстрации системы тестирования я оставил только несколько функций.

Заголовочный файл cstring.h :

#ifndef _CSTRING_H #define _CSTRING_H #define _decl_string_t(N) \ struct < \ int sz; \ char data[N]; \ > typedef _decl_string_t(1) string_t; /** * Объявление строки в форме автоматической переменной в стеке. * Длина строки инициализируется нулем. */ #define decl_string_t(name, size) _decl_string_t(size) name = /** * Создание новой строки в куче. */ string_t* string_new(int sz); /* Трюк с дублированием имен функций, начинающихся с символа '_' * требуется для подавление предупреждений компилятора о преобразовании * типов. */ /** * Удаление строки из кучи. */ #define string_delete(str) _string_delete((string_t*)str) void _string_delete(string_t* str); /** * Текущая длина строки. */ #define string_length(str) _string_length((const string_t*)str) int _string_length(const string_t* str); /** * Изменение длины строки. */ #define string_resize(str, sz) _string_resize((string_t*)str, sz) int _string_resize(string_t* str, int sz); /** * Копирование строки из строки С, завершающейся нулем. */ #define string_from_c_str(dst, src) _string_from_c_str((string_t*)dst, src) string_t* _string_from_c_str(string_t* dst, const char* src); /** * Добавление символа в строку. */ #define string_append_ch(str, ch) _string_append_ch((string_t*)str, ch) string_t* _string_append_ch(string_t* str, char ch); /** * Превращение строки в строку С без добавления нуля на конце. */ #define string_data(str) str->data /** * Превращение строки в строку С с нулем на конце. */ #define string_c_str(str) _string_c_str((string_t*)str) char* _string_c_str(string_t* str); #endif 
#include #include "cstring.h" /** * Подготовительная площадка для тестирования. * Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются * на тестовые. */ #if UNIT_TESTING extern void* _test_malloc(const size_t size, const char* file, const int line); extern void* _test_calloc(const size_t number_of_elements, const size_t size, const char* file, const int line); extern void _test_free(void* const ptr, const char* file, const int line); #define malloc(size) _test_malloc(size, __FILE__, __LINE__) #define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__) #define free(ptr) _test_free(ptr, __FILE__, __LINE__) #endif // UNIT_TESTING /** * Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы * правильно отработать ситуацию, если из-за выравнивания между элементами * структуры string_t 'sz' и 'data' вдруг появится промежуток. */ string_t* string_new(int sz) return malloc(sizeof(string_t) + sz - 1); > /** * Удаление строки из кучи. */ void _string_delete(string_t* str) free((void *)str); > /** * Текущая длина строки. */ int _string_length(const string_t* str) return str->sz; > /** * Изменение длины строки. */ int _string_resize(string_t* str, int sz) return str->sz = sz; > /** * Копирование строки из строки С, завершающейся нулем. */ string_t* _string_from_c_str(string_t* dst, const char* src) int sz = strlen(src); memcpy(dst->data, src, sz); dst->sz = sz; return dst; > /** * Добавление символа в строку. */ string_t* _string_append_ch(string_t* str, char ch) str->data[str->sz++] = ch; return str; > /** * Превращение строки в строку С с нулем на конце. Фактически, * в тело строки добавляется ноль и возвращается указатель на данные. */ char* _string_c_str(string_t* str) str->data[str->sz] = 0; return string_data(str); > 

Как вы заметили, в коде есть специальный блок, ограниченный макросом UNIT_TESTING . Ничего не поделаешь, в языке С приходится “готовить” код к потенциальному тестированию и вставлять фрагменты, позволяющие тестовой среде работать с этим кодом. Этот блок, если задан макрос UNIT_TESTING , переопределяет функции работы с кучей, чтобы можно было перехватывать их вызовы. Подменяющие функции _test_malloc() , _test_calloc() и _test_free() предоставляются библиотекой cmockery .

Теперь файл тестов cstring_unittest.c :

#include #include #include #include #include "cstring.h" /** * Тестируем декларацию строки длиной 20 в виде автоматической * переменной, добавляем в нее два символа, обрезаем строку * до длины в один байт и проверяем, добавился ли 0 при преобразовании * в строку С. */ void string_c_str_test(void **state) decl_string_t(a, 20); a.data[0] = 'a'; a.data[1] = 'b'; a.sz = 1; assert_memory_equal("a\0", string_c_str(&a), 2); > /** * Тестируем изменение длины строки. */ void string_resize_test(void **state) decl_string_t(a, 20); a.sz = 2; string_resize(&a, 1); assert_int_equal(1, string_length(&a)); > /** * Тестируем добавление символа путем сравнения со строками С */ void string_append_ch_test(void **state) decl_string_t(a, 20); assert_string_equal("", string_c_str(&a)); assert_string_equal("a", string_c_str(string_append_ch(&a, 'a'))); assert_string_equal("ab", string_c_str(string_append_ch(&a, 'b'))); > /** * Тестируем декларацию строки в виде автоматической переменной. * Длина строки сразу после декларации должна быть нулевой. */ void string_declare_test(void **state) decl_string_t(a, 20); assert_int_equal(0, string_length(&a)); > /** * Тестируем размещение новой строки в куче и ее удаление из нее. */ void string_heap_allocation_test(void **state) string_t* a = string_new(20); string_delete(a); > /** * Тестируем копирование строки из строки С с нулем на конце. */ void string_from_c_str_test(void **state) string_t* a = string_new(8); string_from_c_str(a, "12345678"); assert_int_equal(8, string_length(a)); string_delete(a); > /** * Создаем список тестов и запускаем их. */ int main(int argc, char* argv[]) const UnitTest tests[] = unit_test(string_declare_test), unit_test(string_c_str_test), unit_test(string_append_ch_test), unit_test(string_heap_allocation_test), unit_test(string_from_c_str_test), unit_test(string_resize_test), >; return run_tests(tests); > 

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

Как я уже сказал, для компиляции потребуются файлы cmockery.c и cmockery.h (см. выше). Эти файлы можно положить в текущий каталог.

Компилируем в Visual Studio:

cl /DUNIT_TESTING /I. cstring_unittest.c cstring.c cmockery.c 

Если все скомпилировалось нормально, то запускаем файл cstring_unittest :

string_declare_test: Starting test string_declare_test: Test completed successfully. string_c_str_test: Starting test string_c_str_test: Test completed successfully. string_append_ch_test: Starting test string_append_ch_test: Test completed successfully. string_heap_allocation_test: Starting test string_heap_allocation_test: Test completed successfully. string_from_c_str_test: Starting test string_from_c_str_test: Test completed successfully. string_resize_test: Starting test string_resize_test: Test completed successfully. All 6 tests passed 

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

Но неинтересно, когда все работает. Внесем в тест библиотеки “случайные ошибки”. Каждую из них можно спокойно допустить непреднамеренно. Строки с ошибками я пометил комментариями со словом “ОШИБКА (!)”. Посмотрим, как cmockery справится с этим.

Файл cstring.c с “ошибками”:

#include #include "cstring.h" /** * Подготовительная площадка для тестирования. * Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются * на тестовые. */ #if UNIT_TESTING extern void* _test_malloc(const size_t size, const char* file, const int line); extern void* _test_calloc(const size_t number_of_elements, const size_t size, const char* file, const int line); extern void _test_free(void* const ptr, const char* file, const int line); #define malloc(size) _test_malloc(size, __FILE__, __LINE__) #define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__) #define free(ptr) _test_free(ptr, __FILE__, __LINE__) #endif // UNIT_TESTING /** * Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы * правильно отработать ситуацию, если из-за выравнивания между элементами * структуры string_t 'sz' и 'data' вдруг появится промежуток. */ string_t* string_new(int sz) return malloc(sizeof(string_t) + 1 - 1); // (ОШИБКА!) "Неверная" длина. > /** * Удаление строки из кучи. */ void _string_delete(string_t* str) // (ОШИБКА!) "Забыли" вызвать free(). > /** * Текущая длина строки. */ int _string_length(const string_t* str) return str->sz; > /** * Изменение длины строки. */ int _string_resize(string_t* str, int sz) return str->sz; // (ОШИБКА!) "Забыли" уменьшить длину строки. > /** * Копирование строки из строки С, завершающейся нулем. */ string_t* _string_from_c_str(string_t* dst, const char* src) int sz = strlen(src); memcpy(dst->data, src, sz); // (ОШИБКА!) "Забыли" присвоить длине новое значение. return dst; > /** * Добавление символа в строку. */ string_t* _string_append_ch(string_t* str, char ch) str->data[str->sz] = ch; // (ОШИБКА!) "Забыли" увеличить длину. return str; > /** * Превращение строки в строку С с нулем на конце. Фактически, * в тело строки добавляется ноль и возвращается указатель на данные. */ char* _string_c_str(string_t* str) // (ОШИБКА!) "Забыли" добавить 0 в конец. return string_data(str); > 

Компилируем и запускаем:

string_declare_test: Starting test string_declare_test: Test completed successfully. string_c_str_test: Starting test difference at offset 1 0x00 0x62 1 bytes of 0x0040f014 and 0x0012fe7c differ ERROR: cstring_unittest.c:19 Failure! string_c_str_test: Test failed. string_append_ch_test: Starting test "ab" != "b" ERROR: cstring_unittest.c:39 Failure! string_append_ch_test: Test failed. string_heap_allocation_test: Starting test Blocks allocated. 0x00326ee0 : cstring.c:27 ERROR: string_heap_allocation_test leaked 1 block(s) string_heap_allocation_test: Test failed. string_from_c_str_test: Starting test Blocks allocated. 0x00326ee0 : cstring.c:27 Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt ERROR: cmockery.c:1379 Failure! string_from_c_str_test: Test failed. string_resize_test: Starting test 0x1 != 0x2 ERROR: cstring_unittest.c:29 Failure! string_resize_test: Test failed. 5 out of 6 tests failed! string_c_str_test string_append_ch_test string_heap_allocation_test string_from_c_str_test string_resize_test Blocks allocated. 0x00326ee0 : cstring.c:27 Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt ERROR: cmockery.c:1379 Failure! 

Бам! 5 из 6 тестов сломаны. Проанализируем полученное.

Тест string_c_str_test выявил, что функция string_c_str не добавила 0 в конец строки, хотя должна была:

string_c_str_test: Starting test difference at offset 1 0x00 0x62 1 bytes of 0x0040f014 and 0x0012fe7c differ ERROR: cstring_unittest.c:19 Failure! string_c_str_test: Test failed. 

Тест string_append_ch_test выявил, что функция добавления символа в конец строки не работает:

string_append_ch_test: Starting test "ab" != "b" ERROR: cstring_unittest.c:39 Failure! string_append_ch_test: Test failed. 

Тест string_heap_allocation_test выявил, что у нас имеется неосвобожденный блок памяти (утечка?). Конечно, мы же “забыли” освободить память в функции string_delete() :

string_heap_allocation_test: Starting test Blocks allocated. 0x00326ee0 : cstring.c:27 ERROR: string_heap_allocation_test leaked 1 block(s) string_heap_allocation_test: Test failed. 

Тест string_from_c_str_test выявил, что мы “вылезли” за границы выделенного куска памяти. Мы записали что-то мимо. Это болезненная ошибка. Конечно, cmockery не всегда может находить такие ляпы. Например, если переменная выделена с стеке, а не в куче, то проблема не вскроется. Тут уже помогут только динамические отладчики типа valgrind:

string_from_c_str_test: Starting test Blocks allocated. 0x00326ee0 : cstring.c:27 Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt ERROR: cmockery.c:1379 Failure! string_from_c_str_test: Test failed. 

Тест string_resize_test показал, что функция изменения размера строки не работает как положено:

string_resize_test: Starting test 0x1 != 0x2 ERROR: cstring_unittest.c:29 Failure! string_resize_test: Test failed. 

В целом, очень неплохие результаты.

Теперь представьте, что вы решили переписать реализацию библиотеки под новый процессор, чтобы работало в десять раз быстрее. Но как проверить результат? Элементарно. Запустите старые тесты. Если они работают, то по крайней мере с большой вероятностью вы не сломали старую функциональность. И, кстати, чем более тщательно написаны тесты, тем более ценны они. Чем более критична какая часть системы для стабильности системы в целом (например, библиотека строк или каких-то базовых контейнеров), тем более тщательно они должны быть покрыты тестами.

Конечно, уровень комфорта при написании тестов на С и их отладке очень далек даже от С++, но это не может быть оправданием для отказа от тестирования. Честно могу сказать, часто результатом работы “сломанного” теста в С, который неверно работает с памятью, например, может является просто зависание, а не красивый отчет, что тест “не работает”. Но даже такой “знак” очень важен и дает понять, что что-то сломано. Пусть лучше повиснет тест, нежели готовый продукт у заказчика.

Под занавес приведу список основных функций-проверок ( assert -фукнции), которые доступны в cmockery :

  • assert_true() , assert_false() — проверка булевых флагов
  • assert_int_equal() , assert_int_not_equal() — сравнение для типа int
  • assert_string_equal() , assert_string_not_equal() — сравнение для типа char* (для С-строк, заканчивающихся нулем)
  • assert_memory_equal() , assert_memory_not_equal() — сравнение кусков памяти
  • assert_in_range() , assert_not_in_range() — проверка нахождения числа в указанном интервале
  • assert_in_set() , assert_not_in_set() — проверка нахождения строки (char*) среди заданного набора строк
  • fail() — безусловное завершения теста с ошибкой

Вывод

Unit-тестирование в С порой сопряжено с трудностями, но оно возможно. И нет причин от него отказываться.

Написание модульных тестов для C/C++ в Visual Studio

С помощью окна Обозреватель тестов вы можете создавать и выполнять модульные тесты для C++. Делается это так же, как и для других языков. Дополнительные сведения об использовании обозревателя тестов см. в разделе Выполнение модульных тестов с помощью обозревателя тестов.

Некоторые функции, такие как Live Unit Testing, закодированные тесты пользовательского интерфейса и IntelliTest, не поддерживаются для C++.

Visual Studio включает в себя эти платформы тестирования C++ без дополнительных загрузок:

  • Платформа модульного тестирования Майкрософт для C++
  • Google Test
  • Boost.Test
  • CTest

Вы можете использовать установленные платформы или написать свой собственный адаптер теста для любой платформы, который вы хотите использовать в Visual Studio. Адаптер теста интегрирует компонент модульных тестов с окном Обозреватель тестов. В Visual Studio Marketplace доступно несколько адаптеров сторонних поставщиков. Дополнительные сведения см. в разделе Установка платформ модульного тестирования сторонних поставщиков.

Visual Studio 2017 и более поздних версий (Professional и Enterprise)

Проекты модульных тестов для C++ поддерживают CodeLens.

Visual Studio 2017 и более поздних версий (все выпуски)

  • Адаптер Google Test включен по умолчанию в рабочую нагрузку Разработка классических приложений на C++. Он содержит шаблон проекта, который можно добавить в решение. Щелкните правой кнопкой мыши узел решения в Обозревателе решений и выберите Добавить>Новый проект в контекстном меню, чтобы добавить шаблон проекта. Параметры адаптера также можно настроить в окне Сервис>Параметры. Дополнительные сведения см. в статье «Практическое руководство . Использование Google Test в Visual Studio».
  • Компонент Boost.Test включен по умолчанию в рабочую нагрузку Разработка классических приложений на C++. Он интегрирован с обозревателем тестов, но в настоящее время не имеет шаблона проекта. Его необходимо настроить вручную. Дополнительные сведения см. в статье «Практическое руководство. Использование Boost.Test в Visual Studio».
  • Поддержка CTest включена в компонент Средства CMake C++, который входит в рабочую нагрузку Разработка классических приложений на C++. Дополнительные сведения см. в разделе «Практическое руководство. Использование CTest в Visual Studio».

Более ранние версии Visual Studio

Вы можете скачать расширения «Адаптер Google Test» и «Адаптер Boost.Test» в Visual Studio Marketplace. Найти их можно на страницах Test Adapter for Boost.Test (Адаптер теста для Boost.Test) и Test Adapter for Google Test (Адаптер теста для Google Test).

Базовый процесс тестирования

В следующих разделах описываются основные действия по началу модульного тестирования для C++. Базовая настройка для платформ Майкрософт и Google Test схожа. Boost.Test требует создать тестовый проект вручную.

Создание тестового проекта в Visual Studio 2022

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

Чтобы добавить новый тестовый проект в существующее решение:

  1. В Обозревателе решений щелкните правой кнопкой мыши узел решения.
  2. Во всплывающем меню выберите пункты Добавить>Новый проект.
  3. Задайте С++ для параметра Язык и введите «тест» в поле поиска. На приведенном ниже рисунке показаны тестовые проекты, доступные при установке рабочей нагрузки Разработка классических приложений на C++ и Разработка для универсальной платформы Windows.

C++ Test Projects in Visual Studio 2022

Создание тестового проекта в Visual Studio 2019

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

  1. В Обозревателе решений щелкните правой кнопкой мыши узел решения.
  2. Во всплывающем меню выберите пункты Добавить>Новый проект.
  3. Задайте С++ для параметра Язык и введите «тест» в поле поиска. На приведенном ниже рисунке показаны тестовые проекты, доступные при установке рабочей нагрузки Разработка классических приложений на C++ и Разработка для универсальной платформы Windows.

C++ Test Projects in Visual Studio 2019

Создание ссылок на другие проекты в решении

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

Add reference

Ссылка на объектный файл или файл библиотеки

Если тестовый код не экспортирует функции, которые требуется протестировать, добавьте выходные OBJ-файлы или LIB-файлы в зависимости тестового проекта. Дополнительные сведения см. в разделе Связывание тестов с объектным файлом или файлом библиотеки. Не включать файлы объектов, имеющие main функцию или другую стандартную точку входа, например wmain , WinMain или DllMain . При добавлении новых исходных файлов в проект обновите зависимости тестового проекта, чтобы включить соответствующие файлы объектов.

Добавление директив #include для файлов заголовков

Далее в CPP-файле модульного теста добавьте директивы #include для всех файлов заголовков, в которых объявляются тестируемые типы и функции. Введите #include » и активирует IntelliSense, чтобы помочь вам выбрать. Повторите эти действия для других заголовков.

Screenshot of the Solution Explorer showing an #include directive being added with IntelliSense highlighting a header file for inclusion.

Чтобы не вводить полный путь в каждой инструкции include в исходном файле, можно добавить необходимые папки в разделе Проект>Свойства>C/C++>Общие>Дополнительные каталоги включаемых файлов.

Написание методов теста

В этом разделе представлен синтаксис при использовании платформы модульного тестирования Майкрософт для C/C++. Он задокументирован в справочнике по API Microsoft.VisualStudio.TestTools.CppUnitTestFramework. Документацию по Google Test см. на странице Google Test Primer (Начало работы с Google Test). Сведения о Boost.Test см. на странице Boost Test Library: The Unit Test Framework (Библиотека Boost.Test: платформа модульного тестирования).

В CPP-файле в тестовом проекте определены класс-заглушка и метод-заглушка. Они служат примером того, как следует писать код теста. В сигнатурах используются макросы TEST_CLASS и TEST_METHOD, что позволяет обнаруживать методы в окне обозревателя тестов.

Screenshot of the Test Explorer window that shows the unittest1.cpp code file containing a stub class and method using the TEST_CLASS and TEST_METHOD macros.

TEST_CLASS и TEST_METHOD являются частью собственной платформы тестирования Microsoft. Обозреватель тестов обнаруживает методы теста в других поддерживаемых платформах аналогичным образом.

TEST_METHOD возвращает пустое значение. Чтобы получить результат теста, используйте статические методы класса Assert для сравнения фактических результатов с ожидаемыми. В приведенном ниже примере предполагается, что MyClass имеет конструктор, принимающий std::string . В этом примере показано, как можно проверить, что конструктор инициализирует класс так, как ожидается:

TEST_METHOD(TestClassInit)

В предыдущем примере результат вызова Assert::AreEqual определяет, пройден ли тест успешно. Класс Assert содержит множество других методов для сравнения ожидаемых и фактических результатов.

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

Запуск тестов

Test Explorer before running tests

  1. В меню Тест выберите пункт Windows>, а затем пункт Обозреватель тестов. На рисунке ниже показан тестовый проект, тесты которого еще не выполнялись.

Примечание. Интеграция CTest с обозревателем тестов пока не доступна. Запустите тесты CTest в главном меню CMake.

Test Explorer after tests are run

  • Если в окне видны не все тесты, выполните сборку тестового проекта, щелкнув правой кнопкой мыши его узел в Обозревателе решений и выбрав Сборка или Перестроить.
  • В обозревателе тестов нажмите Запустить все или выберите тесты, которые следует запустить. Щелкните тест правой кнопкой мыши, чтобы получить доступ к другим командам, включая запуск в режиме отладки с включенными точками останова. После выполнения всех тестов в окне отображаются тесты, которые прошли и которые завершились сбоем.
  • Для завершившихся сбоем тестов приводятся подробные сведения, которые могут помочь установить причину. Щелкните неудачный тест правой кнопкой мыши. Во всплывающем меню выберите команду Отладить для пошагового выполнения функции, в которой произошел сбой.

    Дополнительные сведения об использовании Обозревателя тестов см. в разделе Выполнение модульных тестов с помощью Обозревателя тестов.

    Дополнительные сведения о модульном тестировании см. в статье Основные сведения о модульных тестах.

    Использование CodeLens

    Visual Studio 2017 и более поздних версий (выпуски Professional и Enterprise)

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

    Инициализировать CodeLens для проекта модульного теста C++ можно любым из перечисленных ниже способов.

    • Отредактировать и собрать тестовый проект или решение.
    • Перестроить проект или решение.
    • Запустить тесты из окна обозревателя тестов.

    После инициализации значки состояния тестов отобразятся над каждым модульным тестом.

    C++ CodeLens Icons

    Щелкните значок для получения дополнительных сведений или выполните или отладите модульный тест:

    C++ CodeLens Run and Debug

    Связанный контент

    Юнит тесты на Си — нет ничего проще

    Прочитав статью «Тестирование встроенных систем» и комментарии к ней я был несколько поражен тем фактом, что многие хабровчане знакомы с книгой «Test Driven Development for Embedded C (Pragmatic Programmers)» и framework-ом Unity, но не используют весь арсенал средств, которые предлагают ребята из throwtheswitch.org.

    Хочу кратко поделится опытом использования этих самых средств.

    О себе

    Так получилось, что я нарабатывал свой опыт в программировании встраиваемых систем через тесты (Unit, Integration, System, Stress). За три года мне посчастливилось пройти путь от Junior’a и написания тестов, покрывающих код других специалистов, до Senior’a с опытом разработки систем с использованием TDD методологии.

    Обещанное

    Упомянутый выше framework Unity очень прост и удобен в использовании. Но это всего лишь вершина айсберга. На странице throwtheswitch.org есть следующие инструменты.

    CMock — инструмент позволяющий автоматически генерировать Си-код mock-ов для Ваших тестов. Написан на Ruby. Утверждаю, как человек, который на протяжении трех лет «генерировал» mock-и руками — это просто подарок для Си-разработчика. Но использовать его автономно без следующего инструмента, на мой взгляд, не рационально.

    Ceedling — это целая билд-система, как утверждают сами авторы. Но по сути — это все, что Вам нужно для работы. Данный пакет содержит в себе все необходимое: Unity («тест-раннеры» и «чекалки» значений), CMock (генератор моков) и поддержку командной строки через ruby make.

    Other — под этим странным заголовком находится очень, полезный, на мой взгляд инструмент — CException. Невероятно маленькая библиотека для Си позволяющая получить некое подобие исключений. Но дезинформировать не буду. В проектах использовать не довелось.

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

    Прежде всего, Ceedling должен быть корректно установлен и проверен на работоспособность как указано тут.

    После установки создаем папку и тестовое окружение проекта командой:

    ceedling new MyNewProject 
    • build — сюда будут помещаться все артефакты при сборке и прогоне тестов
    • src- это место для нашего «боевого» кода, который подлежит тестированию
    • test — будут лежать все наши тесты
    • vendor — собственно сам framework, с документацией и плагинами
    • project.yml — конфигурационный файл тестового проекта. Позволяет делать хороший тюнинг, но это с опытом

    Пора писать первый тест.

    Поместим в папку test файл test_calc.c следующего содержания:

    #include "unity.h" #include "calc.h" void setUp(void) < >void tearDown(void) < >void test_add( void )

    Запускаем тест командой:

    ceedling test:test_calc.c 

    Результат ожидаемый. Тест есть, кода нет. Проект не может быть собран.

    Добавляем код.
    В папку src помещаем два файла:

    #ifndef CALC_H #define CALC_H int calc_add(int a, int b); #endif 
    #include "calc.h" int calc_add(int a, int b)

    Повторяем сборку и попытку прогнать тест:

    ceedling test:test_calc.c 

    Если все сделано правильно, то в консоли должны быть результаты теста:

    Test 'test_calc.c' ------------------ Compiling test_calc_runner.c. Compiling test_calc.c. Compiling calc.c. Compiling unity.c. Compiling cmock.c. Linking test_calc.out. Running test_calc.out. ------------------------- OVERALL UNIT TEST SUMMARY ------------------------- TESTED: 1 PASSED: 1 FAILED: 0 IGNORED: 0 

    Этот короткий пример показывает, что test-runner был сгенерирован и добавлен в сборку автоматически. Его код можно найти в папке build/test/runners.

    Попробуем усложнить задачу и предположим, что наш «боевой» файл должен уметь считать только при определенном условии, проверка которого осуществляется в другом программном модуле (например, rules.c). Модифицируем код, для иллюстрации:

    #include "calc.h" #include "rules.h" int calc_add(int a, int b) < if (rules_is_addition_allowed()) < return a + b; >return 0; > 

    Добавим еще один файл в папку src:

    #ifndef RULES_H #define RULES_H int rules_is_addition_allowed(void); #endif 

    Попытка запустить тест будет неудачной, так как нет определения для функции rules_is_addition_allowed().

    Самое время воспользоваться CMock.
    Изменим тест следующим образом:

    #include "unity.h" #include "calc.h" #include "mock_rules.h" void setUp(void) < >void tearDown(void) < >void test_add( void ) < int result = 0; rules_is_addition_allowed_ExpectAndReturn(1); result = calc_add(2,2); TEST_ASSERT_EQUAL_INT( 4, result ); >void test_add_off_nominal( void )

    Таким образом, мы получили автоматически сгенерированный mock одним лишь указанием «#include «mock_rules.h». Исходный код данного файла можно найти в директории build/test/mocks. Его изучение даст хорошее представление о том, каким образом можно менять поведение подменяемого модуля.

    Оговорочки

    1. Я использую данный framework только для тестирования кода на PC. Это диктует определенные правила к архитектуре разрабатываемого ПО. Прогонять юнит тесты на реальном железе смысла не вижу. HAL — он либо работает либо нет и тестируется мануально (мое видение ситуации);
    2. Я не использую данный framework для тестирования нескольких потоков. Потокобезопастность данного инструмента мной не исследовалась;
    3. Данная статья не учит как правильно писать код и/или тесты, а всего-лишь дает краткое представление об упомянутых выше инструментах разработки.

    • Тестирование IT-систем
    • TDD

    Как писать unit тесты си

    В этой статье мы освоим технику TDD, работу с git и github, немного познакомимся с языком C++ и Фреймворком unit-тестирования Catch2

    Следуйте инструкциям. В конце выполните задание, указанное в тексте.

    Создаём каталог проекта

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

    Создайте каталог, в котором вы будете размещать свои проекты. Его можно назвать, например, “lw1” (laboratory work 1)

    Иллюстрация

    В Visual Studio Code откройте этот каталог. Для этого используйте меню “File”>”Open Folder…”.

    Иллюстрация

    Теперь вы можете добавить новый файл в каталог прямо из Visual Studio Code. Попробуйте, это так просто!

    Иллюстрация

    Пишем первую программу

    Создайте каталог try_catch и в нём создайте файл main.cpp . Добавьте в файл код функции main.

    #include int main()  std::cout  <"Hello, World!"  <"\n"; > 

    Обратите внимание, что функция main возвращает тип int — она возвращает операционной системе целочисленный код (0 в случае успешного выполнения, ненулевой код в случае ошибки выполнения). Стандарт C++ разрешает ничего не возвращать из функции main, что мы и сделали.

    Теперь надо скомпилировать код. Откройте встроенный терминал Visual Studio Code горячей клавишей “Ctrl+`” либо через меню:

    Скриншот

    Запустите команду g++ —version , чтобы проверить, что компилятор C++ доступен и функционирует. Результат будет выглядеть примерно так:

    >gcc --version gcc (Ubuntu 7.2.0-1ubuntu1~16.04) 7.2.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE 

    Перейдите в терминале в ранее созданный каталог try_catch . Это можно сделать командой cd try_catch . Если команду не удалось выполнить, введите команду dir , чтобы выяснить, какие подкаталоги находятся в текущем каталоге терминала. Сориентируйтесь и добейтесь, чтобы у вас был каталог try_catch с файлом main.cpp .

    Теперь запустите сборку программы командой g++ main.cpp -o try_catch . Если всё в порядке, то компилятор ничего не напишет — а случае ошибки компилятор написал бы информацию о причине ошибки.

    После этого вы можете запустить консольную команду try_catch — так вы запустите собранную вами программу.

    >try_catch Hello, World! 

    Ошибки компилятора

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

    Замените идентификатор cout на coutt :

    #include int main()  std::coutt  <"Hello, World!"  <"\n"; > 

    Запустите команду компиляции g++ main.cpp -o try_catch

    Теперь компилятор вывел сообщение: это список ошибок, возникших при компиляции

    main.cpp: In function ‘int main()’: main.cpp:5:10: error: ‘coutt’ is not a member of ‘std’ std::coutt  

    Давайте разберём текст ошибки подробно:

    Иллюстрация

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

    Всегда читайте сообщения об ошибках, если только не научились определять ошибку с одного взгляда.

    Разработка через тестирование

    TDD (Test Driven Development) - это подход к написанию кода, при котором перед реализацией какой-либо функциональности пишутся тесты для неё. При таком подходе разработка, например, нового класса происходит циклически, метод за методом:

    Диаграмма

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

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

    Мы разработаем через тестирование класс для работы с вектором из двух элементов. Но перед этим освоим Фреймворк модульного тестирования.

    Готовимся тестировать

    Для тестирования мы будем использовать Фреймворк Catch2, который можно загрузить в виде одного заголовочного файла: github.com/catchorg/Catch2/blob/master/single_include/catch.hpp

    Загрузите этот файл. Создайте каталог libs и поместите туда загруженный файл catch.hpp .

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

    // ! НЕ ДОБАВЛЯЙТЕ ЭТОТ ПРИМЕР В СВОЙ КОД ! // Определение функции Square, возвращающей квадрат заданного целого числа. int Square(int value)  return value * value; > 

    Добавьте в файл main.cpp объявление этой же функции, пока без определения:

    int Square(int value); 

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

    Теперь удалите функцию main и добавьте в начале файла подключение заголовка Catch2:

    // Макрос заставит Catch самостоятельно добавить определение функции main() // Это можно сделать только в одном файле #define CATCH_CONFIG_MAIN #include "../libs/catch.hpp" 

    Снова соберите программу командой g++ main.cpp -o try_catch — сборка должна пройти успешно (без сообщений).

    Теперь добавьте объявление тест-кейса.

    TEST_CASE("Squares are computed", "[Square]")  REQUIRE(Square(1) == 1); REQUIRE(Square(2) == 4); REQUIRE(Square(3) == 9); REQUIRE(Square(7) == 49); REQUIRE(Square(10) == 100); > 

    Это объявление интенсивно использует макросы. Перед компиляцией компилятор заменит все макросы в соответствии с их объявлением, то есть макрос TEST_CASE(. ) будет заменён, скорее всего, на объявление функции, реализующий тест кейс, и код регистрации этой функции в общем наборе тестов. Макрос REQUIRE будет заменён на проверяемое сравнение значений.

    Попробуйте собрать программу — и у вас не получится! Посмотрите на текст ошибки. В нём сказано, что нет ссылки на Square(int) . Это значит, что компоновщик, автоматически вызванный компилятором, смог найти вызовы функции Square , но нигде не смог найти машинный код этой функции. Машинного кода нет, потому что нет и исходного кода — мы разместили только объявление функции без определения.

    Давайте теперь реализуем функцию Square неправильно. Замените объявление функции на следующее определение:

    int Square(int value)  return 1; > 

    Соберите программу. Затем запустите программу try_catch в терминале. Вы увидит сообщения об ошибке в тестах!

    Скриншот

    Давайте теперь напишем черновую, но работоспособную версию Square :

    int Square(int value)  int square = value; square = square * value; return square; > 

    Соберите программу и запустите её в терминале. Все тесты будут успешно пройдены:

    Скриншот

    Но разве это повод останавливаться? Конечно, нет: код функции Square пока ещё далёк от идеала. Его можно сократить, убрав явно излишнюю переменную square:

    int Square(int value)  return value * value; > 

    Снова соберите программу и запустите её в терминале. Все тесты будут успешно пройдены:

    Скриншот

    Только что вы освоили ценный навык: рефакторить код под прикрытием тестов. Если тестов нет, то случайная опечатка или забывчивость в процессе правок могут сломать страшный на вид, но работоспособный код. Под прикрытием тестов поломка будет обнаружена. Этот эффект особенно заметен в динамических языках (вроде JavaScript или Python), но и в C++ вы можете избавиться от мучительной отладки, если просто будете писать автоматические тесты для некоторых задач — таких, как обработка строк, файлов или математические вычисления.

    Структура Vector2f

    Создайте каталог vector2 , и в нём два файла: main.cpp и Vector2f.hpp

    Мы напишем структуру, которая будет хранить декартовы координаты вектора из двух элементов (x, y) . Мы могли бы объявить эту сущность как класс, а не структуру, тем более что в C++ между ключевыми словами class и struct практически нет разницы. Но C++ Core Guidelines не советуют так делать:

    Вектор не имеет внутреннего инварианта: его элементы могут быть изменены независимо друг от друга. Поэтому мы объявим его как структуру. Поместите это объявление в файл “Vector2f.hpp”:

    #pragma once // pragma once защищает от проблемы двойного включения заголовка в файл // подробнее: https://stackoverflow.com/questions/1143936/ // Подключаем заголовок cmath из стандартной библиотеки, он пригодится позже // Документация заголовка: http://en.cppreference.com/w/cpp/header/cmath #include // Объявляем новый тип данных - структуру с названием Vector2f struct Vector2f  // Два поля структуры имеют тип float // Мы явно указываем, что поля в любом случае надо инициализировать нулём. // Использование неинициализированной памяти - одна из самых страшных // ошибок C++ программиста, и её надо всячески избегать. float x = 0; float y = 0; // Конструктор без аргументов инициализирует структуру в той // инструкции, где она объявлена. Нас устраивает реализация // конструктора, предлагаемая компилятором по умолчанию, // поэтому мы написали "= default" Vector2f() = default; // Конструктор с двумя аргументами инициализирует структуру // двумя значениями. Пример: // Vector2 speed(10, 20); Vector2f(float x, float y) : x(x), y(y)  > >; // После объявления структуры следует поставить точку с запятой. // Если этого не сделать, возникнет ошибка компиляции. // Некоторые компиляторы плохо обрабатывают эту ошибку и выдают // много индуцированных ошибок вместо одной правильной. 

    Теперь протестируем конструктор нашего класса — да, от этого мало пользы, но надо же с чего-то начать!

    Перепишите этот код в main.cpp :

    // Макрос заставит Catch самостоятельно добавить определение функции main() // Это можно сделать только в одном файле #define CATCH_CONFIG_MAIN #include "../libs/catch.hpp" // Включаем заголовок, где мы описали структуру #include "Vector2f.hpp" // В C++ есть много способов вызвать один и тот же конструктор. // Мы попробуем большинство из них. TEST_CASE("Can be constructed", "[Vector2f]")  // Обычное конструирование при объявлении. Vector2f v1(1, 2); REQUIRE(v1.x == 1); REQUIRE(v1.y == 2); // Явный вызов конструктора, затем присваивание. Vector2f v2 = Vector2f(-1, 29); REQUIRE(v2.x == -1); REQUIRE(v2.y == 29); // Конструирование списком инициализации (C++11) - более универсальный приём. Vector2f v3 =  5, -11 >; REQUIRE(v3.x == 5); REQUIRE(v3.y == -11); // Универсальное конструирование (C++11) - ещё более универсальное Vector2f v4 18, -110 >; REQUIRE(v4.x == 18); REQUIRE(v4.y == -110); > 

    Для сборки программы достаточно собрать один только “main.cpp”, заголовочные файлы сборке не подлежат, т.к. директива #include в любом случае на время компиляции включает содержимое указанного файла в текущий файл (в нашем случае “main.cpp”).

    Перейдите в терминале в каталог “vector2” командой cd ..\vector2

    Затем соберите программу командой g++ main.cpp -o vector2 и запустите её. Все тесты должны быть пройдены.

    Добавляем метод length

    Метод — эта функция, связанная с объектом. В C++ объект, связанный с методом, передаётся при вызове неявно и доступен через ключевое слово this . Впрочем, оно нам пока не потребуется: все поля объекта также доступны в методе напрямую.

    Длина вектора, согласно теореме Пифагора, вычисляется как квадратный корень из суммы квадратов компонентов вектора. Это справедливо не только для вектора из 2 элементов, но и для векторов любых размерностей.

    Для вычисления длины вектора нам нужно уметь извлекать квадратный корень. Это умеет делать функция std::sqrt. Прочитайте её документацию, затем взгляните на реализацию метода length:

    // ..внутри объявления Vector2f float length() const  return std::sqrt(x * x + y * y); > 

    Вы внимательно прочитали документацию sqrt? В каких случаях в sqrt возникает ошибка? Может ли такая ситуация произойти в функции length?

    Теперь пора протестировать метод:

    TEST_CASE("Can compute length", "[Vector2f]")  // Пифагоровы числа: 3, 4, 5 Vector2f v1 = 3.f, 4.f>; REQUIRE(v1.length() == 5.f); // Пифагоровы числа: 12, 35, 37 Vector2f v2 = 12.f, 35.f>; REQUIRE(v2.length() == 37.f); > 

    Здесь мы использовали опасный приём: сравнение чисел типа float . Из-за погрешностей при работе с числами с плавающей запятой тесты вполне могут провалиться. Для сравнения чисел с плавающей точкой Catch2 предоставляет вспомогательный класс Approx, которым мы воспользуемся:

    TEST_CASE("Can compute length", "[Vector2f]")  // Пифагоровы числа: 3, 4, 5 Vector2f v1 = 3.f, 4.f>; REQUIRE(v1.length() == Approx(5.f)); // Пифагоровы числа: 12, 35, 37 Vector2f v2 = 12.f, 35.f>; REQUIRE(v2.length() == Approx(37.f)); // Пифагоровы числа: 85 132 157 Vector2f v3 = 85.f, -132.f>; REQUIRE(v3.length() == Approx(157.f)); // Пифагоровы числа: 799 960 1249 Vector2f v4 = 799.f, -960.f>; REQUIRE(v4.length() == Approx(1249.f)); // Пифагоровы числа: 893 924 1285 Vector2f v5 = 893.f, -924.f>; REQUIRE(v5.length() == Approx(1285.f)); > 

    Теперь, когда мы протестировали метод “length”, финальным штрихом будет рефакторинг. Иногда стандартная библиотека C++ предоставляет не только базовые средства вроде sqrt, но и продвинутые, подходящие для более конкретных случаев. Функция std::hypot может вычислить гипотенузу по двум катетам, то есть тот же самый корень из суммы квадратов компонентов вектора.

    Замените реализацию Vector2f::length() на предложенную ниже, соберите программу и запустите её, чтобы повторить все тесты и убедиться, что старая и новая реализации работают одинаково.

    float length() const  return std::hypot(x, y); > 

    Добавляем оператор сложения

    Язык C++ позволяет применять операторы +, -, *, /, &&, || и т.д. не только к примитивным типам int, unsigned, bool, float и т.д., но и к пользовательским типам данных. Для этого в языке есть механизм перегрузки операторов. Этим механизмом мы и воспользуемся.

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

    • есть две формы оператора: обычный + и дополняющий (augmented) += ; различие в том, что первый оператор возвращает новое значение, а второй оператор добавляет правое выражение к старому значению левого выражения
    • обычный оператор не меняет значение экземпляра структуры, поэтому он помечен ключевым словом const
    • в языке принято из дополняющего оператора возвращать ссылку на сам объект, чтобы вы могли писать конструкции вида x = y += 5 (хотя писать такой код не рекомендуется)

    Сначала добавим наивную реализацию:

    Vector2f operator+(const Vector2f& other) const  return  0, 0 >; > Vector2f& operator+=(const Vector2f& other)  // Разыменование указателя this позволяет объекту получить ссылку на себя // Оператор не константный, поэтому и ссылка не константная return *this; > 

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

    TEST_CASE("Can sum vectors", "[Vector2f]")  Vector2f v1 = Vector2f3, 5> + Vector2f5, -5>; REQUIRE(v1.x == 8); REQUIRE(v1.y == 0); Vector2f v2 = Vector2f11, -6> + Vector2f-6, 11>; REQUIRE(v2.x == 5); REQUIRE(v2.y == 5); Vector2f v3 = Vector2f11.2f, -6.71f> + Vector2f-6.2f, 11.72f>; REQUIRE(v3.x == Approx(5.f)); REQUIRE(v3.y == Approx(5.01f)); > 

    Теперь можно реализовать операторы сложения, запустить тесты снова и увидеть результат:

    Vector2f operator+(const Vector2f& other) const  return  x + other.x, y + other.y >; > Vector2f& operator+=(const Vector2f& other)  x += other.x; y += other.y; return *this; > 

    Добавляем функцию для скалярного произведения

    Расчёт скалярного произведения — это, как и сложение, бинарная операция над векторами. Однако, для скалярного произведения нет общепринятого символа (иногда используют символ умножения, но это может привести к путанице). Поэтому мы не станем создавать оператор скалярного произведения.

    В англоязычной литературе скалярное произведение обозначают фразой “dot product” или словом “dot” — его мы будем использовать. Однако, стоит ли добавлять метод dot? Если мы это сделаем, код пользователя нашего класса будет выглядеть примерно так:

    Vector2f a =  2, 5 >; Vector2f b =  -3, 1 >; Vector2f c = a.dot(b); 

    Сразу возникает вопрос: почему в этом выражении “a” важнее, чем “b”? Можно ли поменять операнды местами? Являются ли они равнозначными?

    Чтобы не создавать путаницы, мы откажемся от метода и напишем свободную функцию dot. Чтобы соблюсти One Definition Rule (ODR), мы добавим ключевое слово inline.

    Термин “свободная функция” означает, что функция не является методом и не находится внутри определения какой-либо структуры или класса. Эта функция не связана ни с какими объектами и полностью свободна.

    inline float dot(const Vector2f& a, const Vector2f& b)  return 0; > 

    Теперь добавим тесты, соберём программу и запустим её. Тесты должны провалиться.

    TEST_CASE("Calculates dot product", "[Vector2f]")  float d1 = dot(Vector2f3, 5>, Vector2f5, -5>); REQUIRE(d1 == -10); float d2 = dot(Vector2f11, -6>, Vector2f6, 11>); REQUIRE(d2 == 0); float d3 = dot(Vector2f-1, 1>, Vector2f-3, 2>); REQUIRE(d3 == 5); > 

    Остановимся и оглянемся назад

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

    Теперь пришло время помочь своим товарищам: если кто-то ещё не закончил предыдущий этап или закопался в проблемах, нужно его откопать!

    Разрабатываем в команде класс Vector3f

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

    Здесь находится исходный код, с которого можно начать разработку: github.com/ps-group/dive-into-cpp. Вам нужно:

    • зарегистрироваться на github
    • создать fork этого репозитория через веб-интерфейс github
    • клонировать (git clone) репозиторий
    • перейти в каталог “vector3” и собрать тесты командой g++ Vector3f_tests.cpp -o vector3 , затем запустить “vector3”

    Далее в цикле, пока весь класс не будет реализован:

    • определиться с коллегами, какой оператор, метод или функцию возьмёте на себя вы
    • разработать оператор/метод/функцию по принципам TDD
    • добавить изменения в индекс git командой git add
    • зафиксировать изменения, сделав commit (git commit)
    • отправить изменения на удалённый репозиторий (git push origin)
    • в интерфейсе github создать pull request

    Напомним ещё раз, как выглядит цикл TDD:

    Диаграмма

    PS-Group

    • PS-Group
    • sshambir@gmail.com

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *