1. Що таке CI і чому він схожий на «ввічливого робота»
Уявіть, що у вас є друг, який щоразу після вашого коміту мовчки робить три речі: конфігурує проєкт, збирає його, запускає тести — і каже: «усе гаразд» або «зламалося». Це і є CI в найпростішому, побутовому сенсі. Це не магія і не «ще одна мова», а просто дисципліна: запускати перевірку однаково, повторювано й без ручних маніпуляцій.
CI (Continuous Integration) — це підхід, за якого перевірка проєкту запускається автоматично після змін у репозиторії. Зазвичай це відбувається на окремій машині, тобто сервері, але зараз нам важливіше не те, «де», а те, «що саме». CI майже завжди зводиться до пайплайну з кількох команд, які можна виконати й локально. Якщо ви можете зробити це на своєму компʼютері в терміналі однією послідовністю команд, то вже розумієте основу CI, навіть якщо жодного разу не бачили GitHub Actions або Jenkins.
Головна мова CI — не YAML, а код завершення процесу
Поки ви вчитеся програмувати, легко оцінювати успіх так: «Запустив — наче вивело те, що хотів». Для людини це нормальна евристика. Для машини — ні. Їй потрібне коротке й недвозначне правило: успіх або помилка.
Таким правилом у світі командного рядка є exit code (код завершення процесу). Якщо програма завершилася з кодом 0, це зазвичай означає: «усе добре». Якщо вона завершилася з кодом, відмінним від 0, це означає: «сталася помилка». І зверніть увагу: це стосується не лише ваших програм, а й утиліт збирання, тест-ранерів та CTest.
Ось мінімальний «ручний тест-ранер», який добре пояснює ідею CI:
#include <iostream>
bool test_add() {
return (2 + 3) == 5;
}
int main() {
if (!test_add()) {
std::cerr << "test_add FAILED\n";
return 1; // не-0 = провал
}
return 0; // 0 = успіх
}
Якщо таку програму запустить CI, йому не потрібно «читати очима» FAILED. Достатньо побачити код 1.
2. Пайплайн configure → build → test: зміст трьох кроків
Слова configure, build, test звучать як заклинання. Але на практиці це три різні перевірки, і кожна ловить свій клас проблем. Важливо розуміти їхній зміст, бо новачки часто чекають від одного кроку того, що насправді має робити інший.
Нижче — зручна таблиця, яку варто тримати в голові:
| Крок пайплайну | Приклад команди (концептуально) | Що підтверджуємо | Типова помилка |
|---|---|---|---|
| configure | |
проєкт узагалі можна налаштувати: компілятор знайдено, залежності доступні, CMakeLists.txt коректний | «CMake не знайшов…», «помилка синтаксису CMake», «не та версія стандарту» |
| build | |
код компілюється й лінкується | «помилка компіляції», «undefined reference», «не той include» |
| test | |
поведінка не зламана, тести проходять | «упав тест», «регресія», «неправильний результат» |
Щоб закріпити це візуально, ось проста схема:
flowchart LR
A[configure] -->|успіх| B[build]
A -->|помилка| X[ПАЙПЛАЙН НЕ ПРОЙШОВ]
B -->|успіх| C[test]
B -->|помилка| X
C -->|успіх| Y[ПАЙПЛАЙН ПРОЙШОВ]
C -->|помилка| X
Важлива деталь: кожен крок — це окрема команда, і кожна також завершується кодом 0/не‑0. Тому пайплайн — не «складна система», а просто ланцюжок: «якщо попередній крок успішний, запускаємо наступний».
Configure: чому добре, що цей крок може «завалити» проєкт ще до компіляції
Новачки часто недооцінюють крок configure. Здається: «Ну, CMake просто щось там генерує». Насправді configure перевіряє, чи проєкт узагалі описано коректно і чи можна його зібрати в цьому оточенні.
Наприклад, якщо ви випадково припустилися помилки в CMakeLists.txt, то можете отримати збій ще до компіляції C++. І це чудово, бо проблему вдається зловити раніше.
Мінімальний CMakeLists.txt для ілюстрації може виглядати так:
cmake_minimum_required(VERSION 3.20)
project(TaskBook LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
add_executable(taskbook src/main.cpp)
Якщо configure не проходить, це означає, що на цьому компʼютері або в цій конфігурації проєкт поки не вдається зібрати. У CI це особливо важливо, бо CI-машина зазвичай «чиста»: на ній немає ваших локальних обхідних рішень.
Build: чому збирання — це ще й лінкування
Крок build багато хто сприймає так: «Ну, компілятор же скаже, якщо я помилився». Так, але помилки бувають не лише синтаксичні.
Бувають і помилки лінкування: ви оголосили функцію, але забули додати файл до збирання; перейменували .cpp, але не виправили CMake; випадково зробили дві однакові реалізації однієї функції в різних місцях. Для новачка це часто виглядає як «магія, нічого не розумію», але CI якраз корисний тим, що стабільно відтворює проблему.
Показовий приклад типової новачкової ситуації: ви додали новий файл src/parse.cpp, написали там parse_priority, локально в IDE все якось зібралося (наприклад, IDE сама підхопила файл), а в CMake ви забули його додати. На вашому компʼютері все може бути «зеленим» через кеш або особливості IDE, а в CI збирання чесно впаде — одразу й без поблажок.
Test: чому тести — обовʼязковий третій крок
Крок test — це те, що перетворює «воно компілюється» на «воно працює так, як ми домовилися». І найважливіше слово тут — «домовилися». Тести фіксують контракт поведінки.
Якщо ви використовуєте CTest, загальна ідея така: у вас є окремий виконуваний файл із тестами, а CTest його запускає.
Концептуально це виглядає так:
enable_testing()
add_executable(taskbook_tests tests/test_main.cpp)
add_test(NAME taskbook_core COMMAND taskbook_tests)
Після цього команда ctest запускає зареєстровані тести й підбиває підсумок. Якщо впав бодай один тест, ctest завершиться з кодом, відмінним від 0, а отже пайплайн стане «червоним».
І ось тут CI стає цілком зрозумілим навіть новачкові: якщо тести впали, значить, ви зламали поведінку. Не «можливо» і не «здається», а конкретне правило. І тест-ранер зазвичай покаже, де саме.
3. Навіщо CI новачку
У новачка зазвичай є дві великі проблеми: він часто ламає проєкт випадково й не розуміє, де саме це сталося; а ще не певен, що «все працює», бо перевірив лише один сценарій «на око». CI розвʼязує обидві — без філософії, суто технічно.
По-перше, CI вчить вас дивитися на проєкт як на річ, що має відтворювано збиратися не лише «на моєму компʼютері». І це не про «сервери та корпорації», а про просту річ: ви самі за тиждень — уже інша людина, з іншим настроєм, і вона не памʼятає, які кнопки в IDE треба було натиснути, щоб усе запрацювало.
По-друге, CI змушує вас розділити «код компілюється» і «код поводиться правильно». Новачки часто плутають ці речі й дивуються: «Але ж воно зібралося!» Так, зібралося. Це означає лише одне: компілятор зрозумів ваш текст. Але це зовсім не гарантує, що логіка правильна.
По-третє, CI допомагає ловити регресії. Регресія — це коли ви «поліпшили» код, а стара фіча тихо померла. І найнеприємніше — померла не там, де ви щось змінювали, а десь поруч або взагалі в іншому файлі. Автоматичні тести й автоматичний запуск — це ваша сигналізація від таких сюрпризів.
Що означає «CI-дружні тести»
У тестів є неприємна властивість: їх легко написати так, що вони «то проходять, то ні». А новачок і без того живе у світі, де «іноді воно працює». Не варто додавати ще одне джерело містики.
CI-дружній тест — це тест, який за однакових входів дає однаковий результат. Тобто він детермінований і не залежить від зовнішнього світу. На практиці новачки часто випадково роблять тести «живими»: читають std::cin, дивляться на поточний час, використовують випадкові числа без фіксованого seed, залежать від порядку виконання тестів.
Дуже практичне правило: якщо тест для перевірки логіки вимагає від вас «щось увести в консоль», це не unit-тест, а вистава «я вдав користувача». Для CI такі вистави не підходять: він не вміє і не повинен вводити текст.
Трохи менш очевидний приклад: тест, який повністю порівнює рядки виводу разом із зайвими пробілами та перенесеннями, може виявитися надто крихким. Іноді це справді потрібно, але частіше варто тестувати зміст — структуру даних, коди помилок, — а не «красу ASCII-таблички».
Чому CI корисний, навіть якщо ви працюєте самі
Є спокуса думати: «CI потрібен у команді, а я один — навіщо він мені?». На практиці навіть одиночний проєкт виграє від CI, бо у вас зʼявляється зовнішній «обʼєктивний суддя». Він не втомлюється, не поспішає, не забуває запускати тести, не плутає Debug із Release і не каже: «ну наче норм».
Крім того, CI допомагає виробити звичку робити невеликі, безпечні зміни. Якщо ви вносите одну правку й одразу проганяєте пайплайн, то точно знаєте, що зламали, якщо взагалі щось зламали. Якщо ж ви зробили 15 правок, а потім вирішили: «ну давай-но я запущу тести», — ви вже не знаєте, яка саме з них винна. Це не питання таланту, а проста математика кількості змін.
І нарешті, CI дуже допомагає психологічно. Коли пайплайн зелений, робити рефакторинг значно спокійніше. Ви перестаєте ставитися до свого коду як до кришталевої вази, яку страшно чіпати.
4. Практичний приклад: TaskBook і функція, яку зручно тестувати
Щоб усе це не залишалося абстракцією, продовжимо умовний навчальний проєкт: маленький консольний застосунок TaskBook, який зберігає список завдань у памʼяті та вміє додавати завдання, перевіряти дані й друкувати список. Для тестів тут важливо одне: винести «мізки» у функції та/або невеликий модуль, який можна викликати без std::cin.
Нехай у нас є функція в бібліотечній частині, яку зручно тестувати:
#include <string>
#include <optional>
std::optional<int> parse_priority(const std::string& s) {
if (s.empty()) return std::nullopt;
int x = 0;
for (char c : s) {
if (c < '0' || c > '9') return std::nullopt;
x = x * 10 + (c - '0');
}
if (x < 1 || x > 5) return std::nullopt;
return x;
}
З погляду CI це означає таке:
- крок build підтверджує, що функція компілюється разом з усім проєктом;
- крок test підтверджує, що вона поводиться правильно на коректних і некоректних вхідних даних.
5. Локальний міні-пайплайн: «CI без CI»
Дуже корисна думка для новачка: CI — це не обовʼязково «хмарна штука». CI починається з того моменту, коли у вас є одна повторювана команда або скрипт, що виконує configure/build/test і повертає правильний код завершення.
Найтиповіша локальна послідовність команд для CMake-проєкту виглядає так (приклад для out-of-source збирання в папку build):
cmake -S . -B build
cmake --build build
ctest --test-dir build
Це вже «міні-CI», тому що:
- команди запускаються однаково щоразу;
- результат визначається кодом завершення кожної команди;
- ви не залежите від «налаштувань IDE, які десь там заховані в проєкті».
Якщо хочеться зробити з цього одну кнопку, ви можете створити, наприклад, скрипт ci_local.sh:
#!/usr/bin/env bash
set -e
cmake -S . -B build
cmake --build build
ctest --test-dir build
Тут set -e означає: «якщо будь-яка команда завершилася з помилкою, одразу зупини скрипт». Ця поведінка дуже схожа на справжній CI-пайплайн: будь-який збій — і все стає «червоним».
6. Типові помилки
Помилка № 1: вважати, що «build = усе працює».
Збирання підтверджує лише те, що ваш проєкт компілюється і лінкується. Помилка у формулі, неправильна обробка порожнього рядка, переплутана умова валідації — усе це чудово компілюється. Якщо ви не запускаєте тести автоматично, то дуже швидко почнете «ловити баги очима». А це заняття для особливо відданих поціновувачів.
Помилка № 2: тести залежать від вводу, часу або випадковості.
Коли тест читає std::cin, він стає інтерактивним і непридатним для автоматичного запуску. Коли тест залежить від поточної дати чи часу, він може почати падати «за розкладом». Особливо весело, якщо це відбувається раз на добу. Коли тест використовує випадкові числа без фіксованого seed, ви отримуєте «іноді червоне» — найтоксичніший вид помилки.
Помилка № 3: «ну локально ж пройшло» й ігнорування чистого збирання.
Локальне середовище часто містить кеші, зібрані файли та випадково залишені артефакти, які маскують проблему. Тому out-of-source збирання в окрему папку та повторюваний пайплайн такі важливі: вони наближають вашу перевірку до того, що побачить CI на чистій машині.
Помилка № 4: один величезний тестовий сценарій замість набору зрозумілих тестів.
Новачки інколи пишуть один гігантський тест «на все»: додали завдання, потім ще одне, потім надрукували, потім порівняли весь вивід. Якщо такий тест падає, ви не знаєте, що саме зламалося: парсинг, сортування чи формат. Значно надійніше мати кілька коротких тестів, кожен із яких перевіряє одне правило, і запускати їх усі в CI.
Помилка № 5: тести падають, але пайплайн зелений через неправильні коди завершення.
Якщо ваш виконуваний файл із тестами завжди повертає 0, навіть коли щось пішло не так, CI вважатиме, що все добре. Тестові фреймворки та CTest корисні саме тим, що дисциплінують це місце: провал перевірки автоматично перетворюється на ненульовий exit code.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ