Представь, что ты собрал шкаф из IKEA. Все детали на месте, инструкция понятна, шурупы закручены. Но выдержит ли он вес книг? Ты дёргаешь каждую полку. Шатается — подтягиваешь крепление. Держится крепко — отлично, можно загружать. Юнит-тест — это такое же «дёрганье» для твоего кода. Ты проверяешь каждую функцию по отдельности и убеждаешься: она делает именно то, что задумано.
Ты уже умеешь писать код на C++, компилировать его и запускать через терминал. Эти навыки мы освоили в статьях «Первые шаги в C++: ставим среду, пишем и запускаем код» и «Основы командной строки: Bash и PowerShell для разработчиков». Теперь давай научимся доказывать, что твой код работает правильно. Не «вроде бы работает», а точно.
К концу статьи ты напишешь свой первый юнит-тест, проверишь с его помощью алгоритм линейного поиска и соберёшь собственную шпаргалку с командами.
Содержание
- Что такое юнит-тесты и зачем они тебе
- GoogleTest: главные макросы для проверок
- Ставим GoogleTest через CMake FetchContent
- Твой первый тест: проверяем функцию сложения
- Тестируем алгоритм: линейный поиск
- А как в других языках? Python, Java и C# (обзорно)
- Типичные ошибки
- Попробуй сам: три задания
- Шпаргалка
- Что дальше
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, везде есть макросы-проверки. Меняется только синтаксис.
Тестовые утверждения в разных языках
Для сравнения, учить сейчас не нужно.
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. Попробуй сам: три задания
Напиши функцию max(int a, int b), которая возвращает большее из двух чисел. Напиши три теста: a > b, a < b, a == b.
Критерий успеха: три теста проходят, вывод зелёный. Если тест для a == b падает — проверь, что именно возвращает твоя функция при равенстве. См. ошибку №6 (граничные условия).
Напиши функцию long long factorial(int n). Напиши тесты для значений n = 0, n = 1, n = 5, n = 10. Добавь проверку: если n < 0, возвращай -1 (сигнал ошибки). И напиши тест для этого случая — EXPECT_EQ(factorial(-1), -1).
int. Поэтому мы используем тип long long и проверяем значения только до 10. Так мы избегаем переполнения и непонятных ошибок.
Критерий успеха: пять тестов проходят. Если для n = 0 тест падает с сообщением «Expected equality… but was: 0» — ты не обработал особый случай. См. ошибку №6 (граничные условия). Если программа падает с непонятной ошибкой — проверь логику рекурсии или цикла и убедись, что ты корректно обрабатываешь отрицательные n. См. ошибку №3, если ошибка похожа на segmentation fault.
Возьми тест AddPositiveNumbers из раздела 4. Поменяй ожидаемое значение с 5 на заведомо неверное, например 99. Запусти тест. Прочитай красный вывод и опиши своими словами: какое значение GoogleTest ожидал, а какое получил фактически.
Критерий успеха: ты видишь красный вывод и понимаешь, где фактическое значение, а где ожидаемое. Если вывод зелёный — ты недостаточно сломал тест. См. ошибку №1.
9. Шпаргалка
EXPECT_EQ(a, b)a == bEXPECT_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 buildbuildcmake -S . -B buildcmake --build buildcmake --build buildctest --test-dir buildctest --test-dir build10. Что дальше
Ты написал свой первый юнит-тест. Это важный шаг — ты перешёл от «код вроде работает» к «я могу это доказать». Но тесты проверяют только то, что ты явно запрограммировал. А что насчёт ошибок, о которых ты не подумал?
В следующей статье «Безопасность в разработке: основные уязвимости и как их избегать» ты научишься находить места в коде, которые могут сломать твою программу или дать доступ злоумышленнику, и закрывать их. С помощью тестов, которые ты освоил сегодня, ты сможешь проверить, что защита действительно работает. Например, напишешь тест, который убедится, что программа не падает и не выдаёт секретных данных при вводе некорректной строки.
Если хочешь увидеть общую картину и понять, как эта тема встраивается в твой путь к первой работе, загляни в хаб-статью «Путь начинающего разработчика: дорожная карта от нуля до первой работы». А если примеры на Python или C# показались тебе интереснее, чем C++, прочитай статью «Какой язык программирования выбрать для изучения новичку» — она поможет определиться.
Существует подход TDD — разработка через тестирование. При нём тест пишут до кода. Сначала ты пишешь тест, который падает (потому что функции ещё нет), затем — саму функцию, и тест становится зелёным. Это как сначала нарисовать чертёж, а потом собрать деталь и сразу проверить, подходит ли она. Начинающие редко используют TDD сразу, но знать о таком подходе полезно.
Заключение
Юнит-тест — не страшный экзамен, а твой личный робот-проверяльщик. Он берёт рутину на себя и освобождает тебе время для настоящего творчества — написания нового кода. Ты уже знаешь, как писать функции на C++, как собирать проект с CMake и как запускать тесты через ctest. Выполни три задания из «Попробуй сам», чтобы рука запомнила макросы EXPECT и ASSERT. Начни с первого теста. Он уже ждёт тебя.
Эта шпаргалка создана для первых шагов: чтобы не тратить время на поиск команд и макросов в документации. Внутри — все макросы EXPECT и ASSERT с пояснениями, готовый шаблон CMakeLists.txt с FetchContent (копируешь в новый проект и работаешь) и команды терминала для запуска тестов. Распечатай и держи рядом — так рука быстрее запомнит нужные команды.
Скачать PDF