Отладка это вроде бы очевидное свойство тестирования, но мне часто встречались ситуации когда разработчик видел в написании тестов только необходимость самого тестирования. Но unit-тесты могут упростить процесс отладки сложных кейсов, для которых приходиться выполнять много предварительных действий, чтобы достичь отлаживаемого места в коде. Можно конечно модификацией рабочего кода ускорить доступ к проблемному участку, но у этого подхода есть существенный минус - это изменение рабочего кода, из-за чего есть шанс забыть удалить дебажные изменения. Я сторонник применения unit-тестов вместо модификации рабочего кода, и к тому же этот тест “отладки” останется на будущее непосредственно как unit-тест для средств тестирования.
В статье рассмотрены три варианта отладки (в формате Microsoft Visual Studio 2022 solution). Два варианта для embedded проектов, под отладочную плату STM32F4-Discovery, так как во встраиваемом ПО часто сложнее соблюсти все условия для срабатывания отлаживаемого кода. И третий вариант для .NET приложения. Все три решения включают в себя рабочий проект и проект для тестирования:
С/С++.
UTestsForDebug_CAN. Имитация некоторого девайса на STM32F4 с коммуникацией по CAN шине. По легенде пытаемся отладить очень редко исполняемый кусок кода обработки принимаемых команд по CAN. Для отладки будет использована возможность перевести CAN модуль процессора в режим LOOPBACK, в котором передаваемые данные будут поступать в приемную часть.С/С++.
UTestsForDebug_UART. Имитация некоторого девайса на STM32F4 с коммуникацией по UART интерфейсу. По легенде пытаемся отладить очень редко исполняемый кусок кода обработки принимаемых команд по UART. Ввиду отсутствия локального “эха” у модуля UART, для отладки воспользуемся “мокингом (Mock)” функций приема/передачи последовательного порта.C#.
UTestsForDebug_dotNET. Некоторый сервис по расчету значений по сложной формуле. Отлаживаем эту формулу.
Исходный код всех решений есть на гитхаб. Embedded проекты основаны на плагине VisualGDB, который необходимо предварительно установит�� (для ознакомления достаточно скачать 30 дневную триал-версию) и на тестовом фреймворке CppUTest (он входит в состав VisualGDB). Выбор VisualGDB обусловлен возможностью быстрого старта готового embedded проекта под большое количество платформ и с полноценной отладкой на “железе”. CppUTest выбран из-за более простого мокинга и поддержки детектора memleak-ов из “коробки”. Проект .NET основан на .NET 8.0, используется тестовый фреймворк NUnit.
UTestsForDebug_CAN
Открываем UTestsForDebug_CAN/UTestsForDebug_CAN.sln в Visual Studio.
Краткое описание легенды: в проекте имеется модуль
communication.cpp, в методеComm_ProcessMessages()которого происходит прием и обработка команд с передающей стороны. По командеcmd_VeryDifficultпроисходит вызов методаPerform_Command_VeryDifficult, который и необходимо отладить. Но командаcmd_VeryDifficultвызывается только при соблюдений множества условий и поэтому отладка этого метода затруднена.
Упрощенную отладку выполним в отдельном unit-test проекте UTestsForDebug_CAN_Tests.
Создание unit-test проекта.
Жмем в Visual Studio меню->FILE->Add->New Project. В открывшемся мастере нового проекта выбираем “Embedded Project Wizard”:

Указываем имя проекта UTestsForDebug_CAN_Tests и путь в папке UTestsForDebug_CAN.
Далее выбираем Unit Test, MSBuild, CppUTest:

На следующей странице указываем тулчейн ARM и процессор STM32F407VG:

Далее выбираем Empty Project:

На финальной странице необходимо указать метод отладки, для STM32 это обычно ST-Link:

После создания проекта рекомендую переоткрыть solution, для обновления фильтров в проекте.
Тестовый проект использует исходные коды, библиотеки и конфигурации из рабочего проекта. Это минимизирует отличия сред исполнения в рабочем проекте и в тестовом.
Подключение исходных кодов, библиотек и конфигураций рабочего проекта
Для подключения файлов из рабочего проекта, необходимо проделать шаги:
Удаляем из проекта
UTestsForDebug_CAN_Testsфайл:startup_stm32f407xx.c
Создаем раздел-фильтр для файлов рабочего проекта, выделяем проект
UTestsForDebug_CAN_Tests, жмем меню->PROJECT->New Filter, вводим название ProjectSources. Правым кликом на вновь созданном фильтре в контекстном меню выбираем Add->Existing Item…
Добавляем файлы из рабочего проекта:
UTestsForDebug_CAN/UTestsForDebug_CAN/can_module.cpp
UTestsForDebug_CAN/UTestsForDebug_CAN/communication.cpp
UTestsForDebug_CAN/UTestsForDebug_CAN/Src/stm32f4xx_hal_msp.c
UTestsForDebug_CAN/UTestsForDebug_CAN/Src/stm32f4xx_it.c
UTestsForDebug_CAN/UTestsForDebug_CAN/Src/system_stm32f4xx.c
UTestsForDebug_CAN/UTestsForDebug_CAN/Inc/main.h
UTestsForDebug_CAN/UTestsForDebug_CAN/Inc/stm32f4xx_hal_conf.h
UTestsForDebug_CAN/UTestsForDebug_CAN/Inc/stm32f4xx_it.h
все файлы из папки UTestsForDebug_CAN/UTestsForDebug_CAN/BSP/STM32F4xxxx/STM32F4xx_HAL_Driver/Src
UTestsForDebug_CAN/UTestsForDebug_CAN/BSP/STM32F4xxxx/StartupFiles/startup_stm32f407xx.c
Правым кликом на тестовом проекте открываем настройки “VisualGDB Project Properties”, на закладке слева “MSBuild settings”, в поле Preprocessor Macros прописываем “
DEBUG=1;STM32F407VG;STM32F407xx”Там же в настройках проекта, в поле Include Directories вписываем относительные пути к файлам рабочего проекта “
../UTestsForDebug_CAN/BSP/STM32F4xxxx/BSP/STM32F4-Discovery;../UTestsForDebug_CAN/Inc;../UTestsForDebug_CAN/Src;../UTestsForDebug_CAN/BSP/STM32F4xxxx/CMSIS_HAL/Device/ST/STM32F4xx/Include;../UTestsForDebug_CAN/BSP/STM32F4xxxx/CMSIS_HAL/Include;../UTestsForDebug_CAN/BSP/STM32F4xxxx/STM32F4xx_HAL_Driver/Inc”.В поле Linker Script указываем на файл скрипта линкера “
../UTestsForDebug_CAN/BSP/STM32F4xxxx/LinkerScripts/STM32F407VG_flash.lds”.В фильтре Source files создаем два файла, правый клик на фильтре Add->New Item:
“UTestsForDebug_CAN_Tests.cpp”
#include <CppUTest/CommandLineTestRunner.h> #include <stm32f4xx_hal.h> int main(void) { HAL_Init(); const char *p = ""; CommandLineTestRunner::RunAllTests(0, &p); return 0; }“communication_tests.cpp”
#include "main.h" #include <CppUTest/CommandLineTestRunner.h> #include <stdio.h> TEST_GROUP(CommunicationTestGroup){TEST_SETUP(){ SystemClock_Config(); CAN_Init(true); } TEST_TEARDOWN() {} } ; static Led_TypeDef led_On; void BSP_LED_On(Led_TypeDef Led) { led_On = Led; } static Led_TypeDef led_Off; void BSP_LED_Off(Led_TypeDef Led) { led_Off = Led; } static Led_TypeDef led_Toggle; void BSP_LED_Toggle(Led_TypeDef Led) { led_Toggle = Led; } TEST(CommunicationTestGroup, Debug_VeryDifficult_Case) { uint8_t payload[6] = {0, 1, 2, 3, 4, 5}; CAN_SendCommand(TCommandId::cmd_VeryDifficult, 2, payload); Comm_ProcessMessages(); CHECK_EQUAL_TEXT(LED6, led_On, "Perform_Command_VeryDifficult was not called"); CHECK_EQUAL_TEXT(LED5, led_Off, "Perform_Command_VeryDifficult was not called"); }
Признаком удачно созданного проекта, после пересборки проекта, служит появление нового теста в Test Explorer Visual Studio.

который выполниться без ошибок нажатием на кнопку Run.
Для отладки метода Perform_Command_VeryDifficult необходимо установить breakpoint в этом методе и запустить проект UTestsForDebug_CAN_Tests в Debug.

Как видно из Call Stack, попадание в целевой метод произошло по упрощенной схеме, в TEST(CommunicationTestGroup, Debug_VeryDifficult_Case) данные, подготовленные для cmd_VeryDifficult, передаются в CAN приемник. В методе Comm_ProcessMessages данные принимаются, парсятся и затем вызывается Perform_Command_VeryDifficult. Проверка состояния индикаторов в финале теста добавлена для определения успешности работы отлаживаемого метода, чтобы оставить этот тест уже непосредственно как unit-тест.
UTestsForDebug_UART
Открываем UTestsForDebug_UART/UTestsForDebug_UART.sln в Visual Studio.
Легенда похожа на прошлый пример с шиной CAN, необходимо отладить Perform_Command_VeryDifficult.
Упрощенную отладку выполним в отдельном unit-test проекте UTestsForDebug_UART_Tests.
Создание unit-test проекта
Создание тестового проекта аналогично созданию проекта UTestsForDebug_CAN_Tests. За исключением пунктов:
Замена CAN на UART в названиях.
Файл
UTestsForDebug_UART/UTestsForDebug_UART/uart_module.cppне добавлять в проект.В фильтр “
Source files/Device-specific files/Test Framework” добавить файлы для поддержки mocking, макрос $(TESTFW_BASE_LOCAL) по умолчанию указывает на папку с тестовыми фреймворками VisualGDB, т.е. C:\Users\User\AppData\Local\VisualGDB\TestFrameworks\:"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockActualCall.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockExpectedCall.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockExpectedCallsList.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockFailure.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockNamedValue.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockSupport.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockSupportPlugin.cpp"
"$(TESTFW_BASE_LOCAL)\com.sysprogs.unittest.CppUTest\src\CppUTestExt\MockSupport_c.cpp"
Содержимое UTestsForDebug_UART_Tests.cpp идентично UTestsForDebug_CAN_Tests.cpp.
Содержимое communication_tests.cpp:
#include "CppUTest/TestHarness.h"
#include "CppUTestExt/MockSupport.h"
#include <CppUTest/CommandLineTestRunner.h>
#include "main.h"
#include <stdio.h>
TEST_GROUP(CommunicationTestGroup){TEST_SETUP(){SystemClock_Config();
}
TEST_TEARDOWN() { mock().clear(); }
}
;
TEST(CommunicationTestGroup, Debug_VeryDifficult_Case) {
TCommandId id;
id = TCommandId::cmd_VeryDifficult;
mock()
.expectOneCall("UART_HandleReceivingCommands")
.withOutputParameterReturning("id", &id, sizeof(id))
.andReturnValue(true);
mock()
.expectOneCall("UART_SendCommand")
.withParameter("id", TCommandId::cmd_Start)
.withParameter("status", 0);
mock().expectOneCall("BSP_LED_On").withParameter("Led", LED6);
mock().expectOneCall("BSP_LED_Off").withParameter("Led", LED4);
mock().expectOneCall("BSP_LED_Off").withParameter("Led", LED5);
Comm_ProcessMessages();
mock().checkExpectations();
}
/* mocking work module*/
void UART_Init() { mock().actualCall("UART_Init"); }
void UART_SendCommand(TCommandId id, uint8_t status, uint8_t *payload) {
(void)payload;
mock()
.actualCall("UART_SendCommand")
.withIntParameter("id", id)
.withUnsignedIntParameter("status", status);
}
bool UART_HandleReceivingCommands(TCommandId *id, uint8_t *status,
uint8_t *payload, size_t payload_size) {
(void)status;
(void)payload;
(void)payload_size;
return mock()
.actualCall("UART_HandleReceivingCommands")
.withOutputParameter("id", id)
.returnBoolValue();
}
void BSP_LED_On(Led_TypeDef Led) {
mock().actualCall("BSP_LED_On").withIntParameter("Led", Led);
}
void BSP_LED_Off(Led_TypeDef Led) {
mock().actualCall("BSP_LED_Off").withIntParameter("Led", Led);
}
void BSP_LED_Toggle(Led_TypeDef Led) {
mock().actualCall("BSP_LED_Toggle").withIntParameter("Led", Led);
}После внесения всех изменений и пересборки проекта в Test Explorer Visual Studio появиться новый тест “Debug_VeryDifficult_Case”, который должен успешно выполниться по нажатию на Run.
Отладка метода Perform_Command_VeryDifficult аналогична примеру с CAN шиной. Передаваемые по UART данные симулируется при помощи “mocking” функции UART_HandleReceivingCommands,
id = TCommandId::cmd_VeryDifficult;
mock()
.expectOneCall("UART_HandleReceivingCommands")
.withOutputParameterReturning("id", &id, sizeof(id))
.andReturnValue(true);при вызове UART_HandleReceivingCommands в выходные параметры функции будут переданы данные которые были определены в начале теста, “expectOneCall…withOutputParameterReturning…”. Вариации значений в методах withOutputParameterReturning позволяют симулировать различные кейсы. В примере симуляция данных только у аргумента “id”.
Также для проверки успеха тестирования Perform_Command_VeryDifficult замоканы функции:
UART_SendCommand, с ожиданиемidравнымTCommandId::cmd_Startиstatusравным 0.BSP_LED_On, с ожиданием аргументаLedравнымLED6и два срабатывания
BSP_LED_Off, со значениями уLedравнымиLED4иLED5.
Эти проверки позволят использовать этот код уже непосредственно как unit-тест.
UTestsForDebug_dotNET
Открываем UTestsForDebug_dotNET/UTestsForDebug_dotNET.sln в Visual Studio.
Для ускорения доступа к отлаживаемому коду в методе SuperCalc.GetVeryDifficultCompute использован отдельный unit-test проект UTestsForDebug_dotNET.Tests.
Создание unit-test проекта
Жмем в Visual Studio меню->FILE->Add->New Project.
В открывшемся мастере нового проекта выбираем “NUnit Test Project”.
Указываем имя проекта
UTestsForDebug_dotNET.Testsи путь в папкеUTestsForDebug_dotNET.Далее выбираем Framework .NET 8.0 (LTS) и создаем проект.
В Visual Studio меню->PROJECT жмем на Add Project Reference… и в появившемся окне отмечаем проект UTestsForDebug_dotNET.
Переименовываем файл
UnitTest1.csвSuperCalcTests.cs, заполняем его кодом:
namespace UTestsForDebug_dotNET.Tests {
public class SuperCalcTests {
[Test]
public void GetVeryDifficultCompute_Test() {
var value = SuperCalc.GetVeryDifficultCompute(0);
Assert.That(value, Is.EqualTo(0));
value = SuperCalc.GetVeryDifficultCompute(int.MaxValue);
Assert.That(value, Is.EqualTo(4.6116860143471688E+18).Within(1).Ulps);
value = SuperCalc.GetVeryDifficultCompute(int.MinValue);
Assert.That(value, Is.EqualTo(4.61168601821264E+18).Within(1).Ulps);
}
}
}После внесения всех изменений и пересборки проекта в Test Explorer visual studio должен появиться новый тест “GetVeryDifficultCompute_Test”.
Отладка метода SuperCalc.GetVeryDifficultCompute может производится простым Debug-ом этого теста, правый клик на тесте GetVeryDifficultCompute_Test в редакторе и затем выбор Debug Tests.
Debug Tests

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

И затем после входа в GetVeryDifficultCompute поменять значение у аргумента number на необходимое.
Inline редактирование аргумента

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