Отладка это вроде бы очевидное свойство тестирования, но мне часто встречались ситуации когда разработчик видел в написании тестов только необходимость самого тестирования. Но 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-тестов не так сложно. Лучше потерять час времени на добавление проекта, но потом не заниматься подготовкой каждой сессии отладки. Да и все последующие тесты уже могут быть добавлены намного проще. Статья также в будущем позволит не тратить время на объяснение этого подхода для других коллег-разработчиков.