Рабочее место разработчика с открытым проектом на C++ и зелёными галочками, символизирующими пройденные тесты

Представь, что ты собрал шкаф из IKEA. Все детали на месте, инструкция понятна, шурупы закручены. Но выдержит ли он вес книг? Ты дёргаешь каждую полку. Шатается — подтягиваешь крепление. Держится крепко — отлично, можно загружать. Юнит-тест — это такое же «дёрганье» для твоего кода. Ты проверяешь каждую функцию по отдельности и убеждаешься: она делает именно то, что задумано.

Ты уже умеешь писать код на C++, компилировать его и запускать через терминал. Эти навыки мы освоили в статьях «Первые шаги в C++: ставим среду, пишем и запускаем код» и «Основы командной строки: Bash и PowerShell для разработчиков». Теперь давай научимся доказывать, что твой код работает правильно. Не «вроде бы работает», а точно.

К концу статьи ты напишешь свой первый юнит-тест, проверишь с его помощью алгоритм линейного поиска и соберёшь собственную шпаргалку с командами.

1. Что такое юнит-тесты и зачем они тебе

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

Юнит-тест — это такой автомат для твоей функции. Он проверяет один маленький кусочек программы (функцию или метод) в полной изоляции от остального кода. Подал на вход определённые данные — сравнил результат с ожиданием. Совпало — тест зелёный. Не совпало — красный.

Зачем тратить время на тесты? Есть три причины. Первая: ты ловишь ошибки до того, как их увидит пользователь. Вторая: ты перестаёшь проверять код вручную. Это экономит часы. Третья: тесты становятся живой документацией. Новый человек открывает тесты и сразу понимает, что функция должна делать, а что — нет.

В основе любого теста лежит принцип AAA. Три этапа, которые легко запомнить через нашу метафору со шкафом. Arrange — ты раскладываешь детали из коробки, сверяешься с инструкцией. В коде: готовишь входные данные. Act — закручиваешь шурупы, собираешь каркас. В коде: вызываешь тестируемую функцию. Assert — дёргаешь полку и смотришь, держится ли она. В коде: сравниваешь результат с ожиданием.

Ещё два термина, которые пригодятся. Тестовый случай — это проверка одного конкретного сценария. Например: «что вернёт функция сложения, если передать 2 и 3?». Тестовый набор — это группа тестов, которые проверяют одну и ту же функцию с разных сторон.

Схема трёх этапов юнит-тестирования: подготовка, действие, проверка
Принцип AAA в тестировании
Что мы узнали и проверь себя:
  • Юнит-тест — это автоматическая проверка одной функции в изоляции.
  • Принцип AAA: Arrange (подготовка), Act (действие), Assert (проверка).
  • Тестовый случай — один сценарий. Тестовый набор — группа тестов для одной функции.

Проверь себя: Какие три этапа включает паттерн AAA? Чем тестовый случай отличается от тестового набора?

2. GoogleTest: главные макросы для проверок

GoogleTest — это фреймворк для тестирования на C++. Простыми словами, фреймворк — это набор готовых инструментов, которые избавляют тебя от рутины. Не нужно самому писать вывод результатов или подсчитывать количество пройденных тестов. GoogleTest делает это за тебя.

В центре GoogleTest — макросы. Это специальные команды, которые проверяют условия и сообщают о результате. Главных три: EXPECT_EQ, EXPECT_TRUE, EXPECT_FALSE.

EXPECT_EQ(expected, actual) проверяет, что два значения равны. EXPECT_TRUE(condition) проверяет, что условие истинно. EXPECT_FALSE(condition) — что ложно. Например: ты написал функцию add(a, b). EXPECT_EQ(add(2, 3), 5) проверит, что она действительно возвращает 5.

Важный момент: есть два семейства макросов — EXPECT_* и ASSERT_*. Разница в поведении при ошибке. EXPECT — тест продолжается, даже если проверка не прошла. Это как предупреждение на приборной панели автомобиля: лампочка загорелась, но ехать можно. ASSERT — тест немедленно останавливается. Это стоп-кран: дальнейшая работа бессмысленна, потому что данные повреждены.

Минимальный тест на GoogleTest выглядит так:

#include <gtest/gtest.h>
#include "math_utils.h"

TEST(MathUtilsTest, AddPositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);
}

Макрос TEST() объявляет тест. Первый аргумент — имя тестового набора, второй — имя тестового случая. Внутри фигурных скобок — сами проверки.

Что мы узнали и проверь себя:
  • EXPECT_EQ сравнивает значения, EXPECT_TRUE проверяет истинность, EXPECT_FALSE — ложность.
  • ASSERT останавливает тест при ошибке, EXPECT продолжает. Используй ASSERT, когда дальнейшая проверка опасна без выполнения условия.

Проверь себя: В каком случае стоит использовать ASSERT_NE(ptr, nullptr) вместо EXPECT_NE?

3. Ставим GoogleTest через CMake FetchContent

Установка фреймворка — как подключение нового станка к конвейеру. Один раз правильно подсоединил провода, и дальше он работает без сбоев.

Нам понадобится CMake. Простыми словами, CMake — это инструмент, который говорит компилятору, из каких файлов собирать программу и какие библиотеки подключать. Ты описываешь структуру проекта в файле CMakeLists.txt, а CMake превращает это описание в инструкции для компилятора.

Раньше для подключения GoogleTest нужно было вручную скачивать репозиторий, собирать библиотеку, прописывать пути. Сейчас всё проще. Мы используем FetchContent. Простыми словами, FetchContent — это как автоматический курьер. Ты говоришь ему: «Привези GoogleTest версии 1.15.2 из этого репозитория», и он сам съездит, скачает и распакует библиотеку прямо в твой проект.

Создай структуру папок:

my_project/
├── CMakeLists.txt
├── src/
│   ├── math_utils.h
│   └── math_utils.cpp
└── tests/
    └── test_math_utils.cpp

Файл CMakeLists.txt в корне проекта:

cmake_minimum_required(VERSION 3.24)
project(MyProject)

# Подключаем GoogleTest через FetchContent
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.15.2
)
FetchContent_MakeAvailable(googletest)

# Включаем тестирование
enable_testing()

# Собираем исполняемый файл тестов
add_executable(tests tests/test_math_utils.cpp src/math_utils.cpp)
target_link_libraries(tests GTest::gtest_main)

# Автоматически находим и регистрируем тесты
include(GoogleTest)
gtest_discover_tests(tests)

Разберём ключевые строки. FetchContent_Declare говорит CMake, откуда скачать GoogleTest. FetchContent_MakeAvailable скачивает и делает библиотеку доступной. enable_testing() включает режим тестирования — без него ctest не увидит тесты. gtest_discover_tests автоматически находит все тесты в проекте, чтобы ты не регистрировал их вручную.

Осторожно: Простыми словами, enable_testing() — это команда, которая «включает» режим тестирования. Она говорит CMake: «В этом проекте есть тесты, их можно запускать через ctest». Без этой строки проект соберётся, но ctest не увидит тесты и напишет «No tests were found».

Три команды для сборки и запуска:

cmake -S . -B build      # конфигурируем сборку в папку build
cmake --build build       # собираем проект
ctest --test-dir build    # запускаем тесты

  • Windows: используй ctest -C Debug --test-dir build — нужно явно указать конфигурацию.
  • macOS: может потребоваться xcode-select --install для установки инструментов командной строки.
  • Linux: установи компилятор и CMake командой sudo apt install cmake g++.
Что мы узнали и проверь себя:
  • CMake управляет сборкой, FetchContent автоматически скачивает библиотеки.
  • Без enable_testing() тесты компилируются, но не запускаются через ctest.
  • Три главные команды: cmake -S . -B build, cmake --build build, ctest --test-dir build.

Проверь себя: Что произойдёт, если забыть enable_testing() в CMakeLists.txt?

4. Твой первый тест: проверяем функцию сложения

Первый тест — как «Hello, world!» в тестировании. Важен сам факт запуска.

Напишем простую функцию сложения в math_utils.h:

// math_utils.h
int add(int a, int b);

И реализацию в math_utils.cpp:

// math_utils.cpp
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

Теперь тесты в tests/test_math_utils.cpp:

#include <gtest/gtest.h>
#include "math_utils.h"

TEST(MathUtilsTest, AddPositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);
}

TEST(MathUtilsTest, AddNegativeNumbers) {
    EXPECT_EQ(add(-2, -3), -5);
}

TEST(MathUtilsTest, AddMixedNumbers) {
    EXPECT_EQ(add(-2, 3), 1);
}

Запускаем:

cmake -S . -B build
cmake --build build
ctest --test-dir build

Вывод зелёный: [ PASSED ] 3 tests. Отлично.

А теперь — важный шаг. Давай специально сломаем тест. Поменяем EXPECT_EQ(add(2, 3), 5) на EXPECT_EQ(add(2, 3), 99). Запускаем снова и видим красный вывод:

[  FAILED  ] MathUtilsTest.AddPositiveNumbers
Expected equality of these values:
  add(2, 3)
    Which is: 5
  99

Читай вывод сверху вниз. Первая строка — имя упавшего теста. Дальше — что ожидалось и что получилось на самом деле. «Which is: 5» — фактический результат функции. Ниже — ожидаемое значение (99). Они не совпали, поэтому тест красный. Теперь ты знаешь, где именно ошибка, и можешь её исправить. Красный вывод — это не страшно. Это подсказка.

Что мы узнали и проверь себя:
  • Тест — это обычная функция, внутри которой макросы EXPECT сравнивают результат с ожиданием.
  • Зелёный вывод — тесты прошли. Красный — есть расхождение.
  • В красном выводе GoogleTest показывает и фактическое, и ожидаемое значение.

Проверь себя: Что выведет EXPECT_EQ(add(2, 3), 4) и как будет выглядеть сообщение об ошибке?

5. Тестируем алгоритм: линейный поиск

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

В статье «Алгоритмы и структуры данных для начинающих: ваш первый шаг к эффективному коду» мы разбирали линейный поиск. Давай напишем для него тесты.

Функция linear_search принимает массив и искомое значение. Возвращает индекс элемента или -1, если элемент не найден:

#include <vector>

int linear_search(const std::vector<int>& arr, int target) {
    for (int i = 0; i < arr.size(); i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}

Теперь напишем пять тестов — для разных сценариев:

TEST(SearchTest, ElementFound) {
    std::vector<int> arr = {10, 20, 30, 40, 50};
    EXPECT_EQ(linear_search(arr, 30), 2);
}

TEST(SearchTest, ElementNotFound) {
    std::vector<int> arr = {10, 20, 30};
    EXPECT_EQ(linear_search(arr, 99), -1);
}

TEST(SearchTest, EmptyArray) {
    std::vector<int> arr = {};
    EXPECT_EQ(linear_search(arr, 10), -1);
}

TEST(SearchTest, FirstElement) {
    std::vector<int> arr = {10, 20, 30};
    EXPECT_EQ(linear_search(arr, 10), 0);
}

TEST(SearchTest, LastElement) {
    std::vector<int> arr = {10, 20, 30};
    EXPECT_EQ(linear_search(arr, 30), 2);
}

Мы проверили обычный случай (элемент найден), случай отсутствия элемента, пустой массив, первый элемент и последний. Это граничные условия — крайние случаи, на которых код часто ломается. Если тестировать только «хорошие» данные, программа может упасть в реальной работе при пустом вводе или нулевом значении.

Визуализация алгоритма линейного поиска, который тестируется юнит-тестом
Линейный поиск и юнит-тесты
Попробуй сам (промежуточный):

Напиши тест для линейного поиска в массиве из одного элемента. Проверь два случая: элемент найден и элемент не найден. Если массив состоит из одного элемента и этот элемент — искомый, какой индекс вернёт функция? Напиши тест, который явно проверяет этот случай. И напиши второй тест — для случая, когда элемент не найден в массиве из одного элемента.

Что мы узнали и проверь себя:
  • Тестировать нужно не только «хорошие» случаи, но и граничные условия: пустой массив, первый и последний элементы.
  • Граничные условия — это крайние значения, на которых код часто ведёт себя не так, как ожидается.

Проверь себя: 1) Почему недостаточно протестировать поиск только на непустом массиве? 2) Какой тест ты добавишь для функции деления divide(a, b), чтобы проверить граничное условие? (Подсказка: подумай про деление на ноль.)

6. А как в других языках? Python, Java и C# (обзорно)

Автомобили разных марок устроены по-разному, но руль, педали и коробка передач работают по одному принципу. Так и с тестами. Освоив GoogleTest, ты узнаешь знакомые черты в любом другом фреймворке. Везде принцип AAA, везде есть макросы-проверки. Меняется только синтаксис.

Сравнение синтаксиса тестовых утверждений в C++, Python, Java и C#
Тестовые утверждения в разных языках

Для сравнения, учить сейчас не нужно.

def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5

Запуск одной командой: pytest. Тесты — это функции, имя которых начинается с test_. Фреймворк сам их находит и запускает. Тебе не нужно ничего регистрировать вручную. Компиляция не нужна. Python выполняет код и сразу выдаёт результат.

Для сравнения, учить сейчас не нужно.

Java (JUnit):

@Test
void addsTwoNumbers() {
    assertEquals(5, add(2, 3));
}

C# (xUnit):

[Fact]
public void AddsTwoNumbers() {
    Assert.Equal(5, Add(2, 3));
}

В Java аннотация @Test говорит фреймворку «это тест». В C# атрибут [Fact] делает то же самое. Разные слова — один смысл. Везде принцип AAA: Arrange (подготовить данные), Act (вызвать функцию), Assert (сравнить результат с ожиданием). Меняется только способ записи.

Что мы узнали и проверь себя:
  • Во всех языках есть тестовые фреймворки, и все они работают по принципу AAA.
  • Python: assert, Java: assertEquals, C#: Assert.Equal — разные слова, одинаковая суть.

Проверь себя: Какой макрос в GoogleTest делает то же самое, что assert add(2, 3) == 5 в Python?

7. Типичные ошибки

Ошибка №1: Тест проходит, хотя функция сломана

  • Как выглядит: в тесте написано EXPECT_EQ(add(2,3), add(2,3)) — функция сравнивается сама с собой. Или внутри теста вообще нет ни одного EXPECT/ASSERT.
  • Как понять: ты меняешь функцию на заведомо неправильную, а тест всё равно зелёный.
  • Индикатор: EXPECT_EQ сравнивает функцию саму с собой, а не с ожидаемым значением.
  • Как исправить: всегда проверяй конкретное значение: EXPECT_EQ(add(2,3), 5).

Ошибка №2: Забыт enable_testing()

  • Как выглядит: всё компилируется, но ctest выводит «No tests were found».
  • Как понять: команда ctest не видит ни одного теста.
  • Индикатор: вывод ctest пуст или содержит фразу «No tests were found».
  • Как исправить: добавить строку enable_testing() в CMakeLists.txt перед определением исполняемого файла тестов.

Ошибка №3: Segmentation fault в тесте

  • Как выглядит: программа падает с сообщением «Segmentation fault» или «SIGSEGV», даже не добравшись до EXPECT.
  • Как понять: ошибка происходит на уровне операционной системы, тест не доходит до проверок.
  • Индикатор: в выводе терминала сообщение «Segmentation fault».
  • Как исправить: проверить выход за границы массива в тестируемой функции. Убедиться, что массив не пуст перед обращением к элементу.

Ошибка №4: Один тест проверяет слишком много

  • Как выглядит: внутри одного TEST() пять разных сценариев с десятью EXPECT.
  • Как понять: тест падает, и непонятно, какой именно сценарий сломан.
  • Индикатор: название теста расплывчатое — «TestEverything», «GeneralTest».
  • Как исправить: разделить на несколько TEST() с говорящими названиями: ElementFound, EmptyArray, LastElement.

Ошибка №5: Тесты пишутся после всего кода

  • Как выглядит: проект написан полностью, тестов нет. Мысль: «Потом как-нибудь протестирую».
  • Как понять: функция делает 10 разных вещей, её трудно протестировать изолированно.
  • Индикатор: ты откладываешь написание тестов третью неделю подряд.
  • Как исправить: начать с малого. Прямо сегодня напиши один тест для одной функции. Один тест лучше, чем ноль.

Ошибка №6: Игнорирование граничных условий

  • Как выглядит: тесты только для позитивных чисел и непустых массивов.
  • Как понять: программа падает в реальной работе, когда пользователь вводит ноль или оставляет поле пустым.
  • Индикатор: все тесты зелёные, но на реальных данных функция ломается.
  • Как исправить: для каждой функции проверять пустой ввод, ноль, отрицательные числа, первый и последний элементы. Вернись к разделу 5 и посмотри, как мы тестировали граничные условия поиска.
Наглядное изображение шести типичных ошибок при написании юнит-тестов
6 типичных ошибок в тестировании

8. Попробуй сам: три задания

Задание 1: Функция максимума.

Напиши функцию max(int a, int b), которая возвращает большее из двух чисел. Напиши три теста: a > b, a < b, a == b.

Критерий успеха: три теста проходят, вывод зелёный. Если тест для a == b падает — проверь, что именно возвращает твоя функция при равенстве. См. ошибку №6 (граничные условия).

Задание 2: Факториал.

Напиши функцию long long factorial(int n). Напиши тесты для значений n = 0, n = 1, n = 5, n = 10. Добавь проверку: если n < 0, возвращай -1 (сигнал ошибки). И напиши тест для этого случая — EXPECT_EQ(factorial(-1), -1).

Осторожно: Простыми словами, факториал растёт очень быстро — как снежный ком. Уже 13! не помещается в обычный int. Поэтому мы используем тип long long и проверяем значения только до 10. Так мы избегаем переполнения и непонятных ошибок.

Критерий успеха: пять тестов проходят. Если для n = 0 тест падает с сообщением «Expected equality… but was: 0» — ты не обработал особый случай. См. ошибку №6 (граничные условия). Если программа падает с непонятной ошибкой — проверь логику рекурсии или цикла и убедись, что ты корректно обрабатываешь отрицательные n. См. ошибку №3, если ошибка похожа на segmentation fault.

Задание 3: Специально сломай.

Возьми тест AddPositiveNumbers из раздела 4. Поменяй ожидаемое значение с 5 на заведомо неверное, например 99. Запусти тест. Прочитай красный вывод и опиши своими словами: какое значение GoogleTest ожидал, а какое получил фактически.

Критерий успеха: ты видишь красный вывод и понимаешь, где фактическое значение, а где ожидаемое. Если вывод зелёный — ты недостаточно сломал тест. См. ошибку №1.

9. Шпаргалка

Макрос / Команда
Что делает
Когда использовать
Пример
EXPECT_EQ(a, b)
Проверяет, что a == b
Когда сравниваешь два значения
EXPECT_EQ(add(2,3), 5)
EXPECT_TRUE(cond)
Проверяет, что cond истинно
Когда нужен нестандартный критерий
EXPECT_TRUE(!vec.empty())
EXPECT_FALSE(cond)
Проверяет, что cond ложно
Когда ожидаешь ложь
EXPECT_FALSE(list.isEmpty())
ASSERT_NE(ptr, nullptr)
Проверяет, что указатель не нулевой, иначе стоп
Когда дальше без указателя нельзя
ASSERT_NE(data, nullptr)
cmake -S . -B build
Конфигурирует сборку в папку build
В начале работы над проектом
cmake -S . -B build
cmake --build build
Собирает проект
После изменений в коде
cmake --build build
ctest --test-dir build
Запускает тесты
После каждой сборки
ctest --test-dir build

10. Что дальше

Ты написал свой первый юнит-тест. Это важный шаг — ты перешёл от «код вроде работает» к «я могу это доказать». Но тесты проверяют только то, что ты явно запрограммировал. А что насчёт ошибок, о которых ты не подумал?

В следующей статье «Безопасность в разработке: основные уязвимости и как их избегать» ты научишься находить места в коде, которые могут сломать твою программу или дать доступ злоумышленнику, и закрывать их. С помощью тестов, которые ты освоил сегодня, ты сможешь проверить, что защита действительно работает. Например, напишешь тест, который убедится, что программа не падает и не выдаёт секретных данных при вводе некорректной строки.

Если хочешь увидеть общую картину и понять, как эта тема встраивается в твой путь к первой работе, загляни в хаб-статью «Путь начинающего разработчика: дорожная карта от нуля до первой работы». А если примеры на Python или C# показались тебе интереснее, чем C++, прочитай статью «Какой язык программирования выбрать для изучения новичку» — она поможет определиться.

А знаете ли вы?

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

Заключение

Юнит-тест — не страшный экзамен, а твой личный робот-проверяльщик. Он берёт рутину на себя и освобождает тебе время для настоящего творчества — написания нового кода. Ты уже знаешь, как писать функции на C++, как собирать проект с CMake и как запускать тесты через ctest. Выполни три задания из «Попробуй сам», чтобы рука запомнила макросы EXPECT и ASSERT. Начни с первого теста. Он уже ждёт тебя.

Скачай PDF-шпаргалку «Основные макросы GoogleTest и команды CMake»

Эта шпаргалка создана для первых шагов: чтобы не тратить время на поиск команд и макросов в документации. Внутри — все макросы EXPECT и ASSERT с пояснениями, готовый шаблон CMakeLists.txt с FetchContent (копируешь в новый проект и работаешь) и команды терминала для запуска тестов. Распечатай и держи рядом — так рука быстрее запомнит нужные команды.

Скачать PDF