Unity — юнит-тесты для C, которые не бесят

29 May, 2026
5,264
🔱 1,115
👥 161

Знакомая ситуация? Вы пишете на 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);, и мы хотим ее протестировать.

  1. Скачиваем файлы unity.c, unity.h, unity_internals.h из репозитория и кладем их в свой проект.

  2. Создаем файл теста, например, test_calculator.c.

  3. Пишем сам тест:

    #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();
    }
    
  4. Компилируем наш тест вместе с исходным кодом и файлом unity.c:

    gcc -Ipath/to/unity/src test_calculator.c calculator.c path/to/unity/src/unity.c -o test_runner
    
  5. Запускаем и видим результат. Если все хорошо, программа молча завершится с кодом 0. Если тест упадет, вы увидите подробный отчет в консоли.

Интересно, что авторы Unity предлагают и следующий шаг для автоматизации — инструмент Ceedling. Это система сборки на базе Ruby, которая берет на себя компиляцию, запуск тестов, генерацию моков и отчетов. Если вам понравится Unity, обязательно взгляните и на Ceedling.

Выводы: кому и зачем?

Unity — это глоток свежего воздуха в мире тестирования C. Он не пытается быть комбайном, который умеет все. Это скорее острый и удобный скальпель: делает одну вещь, но делает ее отлично.

Этот фреймворк идеально подойдет, если вы:

  • Пишете под микроконтроллеры и не можете позволить себе тяжелые зависимости.
  • Создаете небольшую C-библиотеку и хотите покрыть ее тестами без лишних хлопот.
  • Хотите внедрить TDD в свой рабочий процесс, но боитесь сложности инструментов.

Конечно, если вам нужны продвинутые возможности вроде автоматической генерации моков или глубокой интеграции с C++ кодом, возможно, стоит посмотреть на более мощные решения. Но для огромного пласта задач в мире C, простоты и мощи Unity хватает с головой.

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

🍪 Мы используем файлы cookie и сервис аналитики Яндекс.Метрика, чтобы сайт работал лучше. Продолжая пользоваться devtrends.ru, вы соглашаетесь с обработкой данных согласно Политике конфиденциальности.