Unity — юнит-тесты для C, которые не бесят
Знакомая ситуация? Вы пишете на C, возможно даже для микроконтроллеров. Код становится сложнее, логика ветвится, и в какой-то момент вы понимаете, что без тестов дальше двигаться рискованно. И тут начинается боль: подключение громоздких фреймворков, проблемы с зависимостями, сложная настройка сборочной системы... Иногда кажется, что настройка тестового окружения отнимает больше времени, чем написание самого кода.
А что, если я скажу, что есть способ тестировать C-код почти так же просто, как написать printf("Hello, World!")? Сегодня мы посмотрим на Unity — фреймворк, который возвращает веру в простое и эффективное юнит-тестирование на C.
Что такое Unity и почему он не про игры?
Сразу оговоримся: это не тот Unity, на котором делают игры. Это маленький, но очень гордый фреймворк для юнит-тестирования, созданный специально для языка C. Его главная философия — минимализм и портативность.
В чем его главная фишка? Он состоит всего из одного C-файла и двух заголовочных файлов. Да, вы не ослышались. Никаких сложных установок, зависимостей или необходимости изучать новую систему сборки. Вы просто добавляете три файла в свой проект, подключаете заголовок и начинаете писать тесты.
Это делает Unity идеальным кандидатом для:
- Встраиваемых систем (Embedded): Где каждый байт на счету, а зависимости — источник головной боли.
- Небольших библиотек и утилит: Когда не хочется тащить тяжелый тестовый фреймворк ради проверки пары функций.
- Обучения TDD в C: Низкий порог входа позволяет сосредоточиться на методике тестирования, а не на инструментах.
Ключевые возможности: что под капотом?
Несмотря на свою простоту, Unity предлагает на удивление богатый набор инструментов для проверки вашего кода. Давайте пробежимся по самым интересным.
1. Простая и понятная структура тестов
Все тесты строятся по единому шаблону, который легко запомнить. У вас есть функции setUp() и tearDown(), которые выполняются до и после каждого теста соответственно, и сами тестовые функции.
#include "unity.h"
// Выполняется перед каждым тестом
void setUp(void) {
// Например, инициализация ресурсов
}
// Выполняется после каждого теста
void tearDown(void) {
// Например, освобождение ресурсов
}
void test_my_function_should_do_something(void) {
// Здесь сам тест
TEST_ASSERT_TRUE(1);
}
Просто, чисто и без магии.
2. Богатый арсенал ассертов
Вот где Unity действительно сияет. Вместо стандартного набора из assert_true и assert_equal, фреймворк дает вам макросы практически на все случаи жизни.
-
Работа с числами: Кроме очевидных
TEST_ASSERT_EQUAL_INT, есть и более интересные варианты. Например, проверка нахождения числа в диапазоне или сравнение с выводом в HEX:// Проверить, что actual находится в пределах +/- delta от expected TEST_ASSERT_INT_WITHIN(10, 100, 105); // Сравнить и вывести результат в hex, если тест упадет TEST_ASSERT_EQUAL_HEX8(0xAB, my_register_value); -
Битовые операции: Это просто находка для embedded-разработчиков! Больше не нужно писать громоздкие конструкции для проверки отдельных битов в регистрах.
uint8_t flags = 0b10101010; // Проверить, что все биты под маской установлены в 1 TEST_ASSERT_BITS_HIGH(0b00001010, flags); // Проверить, что 7-й бит установлен TEST_ASSERT_BIT_HIGH(7, flags); // Проверить, что 0-й бит сброшен TEST_ASSERT_BIT_LOW(0, flags); -
Массивы, строки и память: Сравнить два массива или блока памяти — не проблема.
uint8_t expected_data[] = {1, 2, 3, 4}; uint8_t actual_data[] = {1, 2, 9, 4}; // Ошибка в 3-м элементе // Сравнить два массива, указав количество элементов TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_data, actual_data, 4); // Сравнить два блока памяти TEST_ASSERT_EQUAL_MEMORY(expected_buffer, actual_buffer, 128);
3. Кастомные сообщения об ошибках
Иногда стандартного вывода "expected X, but was Y" недостаточно. К любому ассерту в Unity можно добавить суффикс _MESSAGE и передать свою строку. Это невероятно удобно для отладки тестов, которые упали в CI/CD через полгода после того, как вы их написали.
TEST_ASSERT_EQUAL_INT_MESSAGE(0, result, "Функция должна была вернуть 0 при неверных аргументах!");
Практическое применение: как начать?
Давайте представим, что у нас есть простая функция int multiply(int a, int b);, и мы хотим ее протестировать.
-
Скачиваем файлы
unity.c,unity.h,unity_internals.hиз репозитория и кладем их в свой проект. -
Создаем файл теста, например,
test_calculator.c. -
Пишем сам тест:
#include "unity.h" #include "calculator.h" // Файл с нашей функцией multiply void setUp(void) {} void tearDown(void) {} void test_multiply_should_return_correct_product(void) { TEST_ASSERT_EQUAL_INT(10, multiply(2, 5)); TEST_ASSERT_EQUAL_INT(0, multiply(5, 0)); TEST_ASSERT_EQUAL_INT(-15, multiply(-3, 5)); } // Тестовый раннер int main(void) { UNITY_BEGIN(); RUN_TEST(test_multiply_should_return_correct_product); return UNITY_END(); } -
Компилируем наш тест вместе с исходным кодом и файлом
unity.c:gcc -Ipath/to/unity/src test_calculator.c calculator.c path/to/unity/src/unity.c -o test_runner -
Запускаем и видим результат. Если все хорошо, программа молча завершится с кодом 0. Если тест упадет, вы увидите подробный отчет в консоли.
Интересно, что авторы Unity предлагают и следующий шаг для автоматизации — инструмент Ceedling. Это система сборки на базе Ruby, которая берет на себя компиляцию, запуск тестов, генерацию моков и отчетов. Если вам понравится Unity, обязательно взгляните и на Ceedling.
Выводы: кому и зачем?
Unity — это глоток свежего воздуха в мире тестирования C. Он не пытается быть комбайном, который умеет все. Это скорее острый и удобный скальпель: делает одну вещь, но делает ее отлично.
Этот фреймворк идеально подойдет, если вы:
- Пишете под микроконтроллеры и не можете позволить себе тяжелые зависимости.
- Создаете небольшую C-библиотеку и хотите покрыть ее тестами без лишних хлопот.
- Хотите внедрить TDD в свой рабочий процесс, но боитесь сложности инструментов.
Конечно, если вам нужны продвинутые возможности вроде автоматической генерации моков или глубокой интеграции с C++ кодом, возможно, стоит посмотреть на более мощные решения. Но для огромного пласта задач в мире C, простоты и мощи Unity хватает с головой.
Это отличный пример того, как хороший инструмент должен решать проблему, а не создавать новые. Настоятельно рекомендую.
