Курс ЛР4 · Місія 4
Академічна↔ Сюжет С.І.Д.СРС до ЛР⌨️ команди 7

Лабораторна робота № 4. Перший проєкт на Cargo з юніт-тестом

Мета

Навчитися створювати, збирати й тестувати проєкт на Rust за допомогою інструменту Cargo: ініціалізувати структуру проєкту, підключати зовнішню залежність (crate) і писати юніт-тести, зокрема з підміною (моком) вхідних даних через трейти.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Теоретичні відомості

Cargo — офіційний менеджер збірки й пакетів Rust, що постачається разом із rustc через rustup. Він виконує чотири базові операції: cargo new створює структуру нового проєкту; cargo build компілює проєкт (без прапорця — у debug-режимі з символами налагодження й без оптимізацій; з --release — з повними оптимізаціями); cargo run компілює й одразу запускає бінарник; cargo test компілює й запускає всі функції, позначені атрибутом #[test].

Стандартна структура бінарного проєкту:

lockpick/
├── Cargo.toml
├── Cargo.lock
└── src/
    └── main.rs

Cargo.toml — маніфест: секція [package] задає назву, версію (семантичне версіонування major.minor.patch) і edition (набір мовних правил компілятора; станом на 2026 рік актуальні edition = "2021" і новіша edition = "2024" — новий проєкт, створений cargo new, отримує найновішу стабільну edition за замовчуванням). Секція [dependencies] перелічує зовнішні бібліотеки — crate — які завантажуються з реєстру crates.io. Cargo.lock фіксує точно резолвнуті версії всього дерева залежностей, гарантуючи відтворювану збірку на будь-якій машині з тим самим Cargo.lock.

Юніт-тест — функція, позначена атрибутом #[test], зазвичай розміщена в підмодулі #[cfg(test)] mod tests { use super::*; ... }. Атрибут #[cfg(test)] — умова компіляції: цей код збирається лише коли Cargo викликано з ціллю test, тож тестовий код ніколи не потрапляє у фінальний бінарник, зібраний командою cargo build/cargo run. Тест провалюється, якщо тіло функції панікує — найчастіше через макроси assert!(умова), assert_eq!(ліворуч, праворуч), assert_ne!(ліворуч, праворуч), кожен з яких (крім assert!) при провалі друкує обидва значення, що суттєво спрощує діагностику порівняно з голим assert!(a == b).

Тест може також мати сигнатуру fn some_test() -> Result<(), E> замість (); тоді всередині тесту працює оператор ?, а провал позначається поверненим Err замість паніки — це особливо зручно для тестів, що самі виконують операції, здатні провалитись (парсинг, введення-виведення).

Мок (mock) — підставна реалізація залежності, яка повертає заздалегідь визначений результат замість звернення до реального (мережевого, файлового, часового) джерела, роблячи тест швидким і детермінованим. У Rust немає окремого фреймворка, обов'язкового для мокування: базовий підхід — визначити trait, яким користується логіка, а в тестовому модулі реалізувати цей трейт структурою з фіксованою поведінкою:

trait DataSource {
    fn fetch(&self) -> String;
}

fn process(source: &dyn DataSource) -> String {
    source.fetch().to_uppercase()
}

Основна функція process працює через &dyn DataSource (об'єкт трейту, динамічна диспетчеризація) і не знає, чи це реальне джерело, чи тестовий підмінник — це і є суть інверсії залежностей у Rust без окремих бібліотек для мокування.

Обладнання та програмне забезпечення

Порядок виконання

  1. Відкрити Codespaces/devcontainer лабораторної. Переконатися, що cargo --version виводить версію інструменту без помилок.
  2. Виконати cargo new lockpick --bin (або скористатися наданим стартовим кодом — див. starter/README.md). Очікуваний результат: створено каталог з Cargo.toml і src/main.rs.
  3. Реалізувати функцію fn generate_key(seed: &str) -> String і main(), що її викликає. Зібрати командою cargo build — очікуваний результат: збірка без помилок компіляції.
  4. Запустити cargo run і переконатися, що програма виводить непорожній рядок у stdout.
  5. Написати перший юніт-тест у модулі #[cfg(test)] mod tests { ... } і запустити cargo test — очікуваний результат: test result: ok. N passed; 0 failed.
  6. (Рівень 2) Додати залежність у Cargo.toml (наприклад, sha2 = "0.10") і фактично використати її всередині generate_key, а не лише оголосити в маніфесті.
  7. (Рівень 3) Ввести trait DataSource для джерела вхідних даних, написати мок-реалізацію цього трейту в тестовому модулі й тест, що використовує мок замість реального джерела, покриваючи і успішний, і помилковий сценарій.

Завдання за рівнями

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Рівень 1 (оцінка 3)

Ініціалізувати проєкт на Cargo, реалізувати структуру (generate_key і main), зібрати (cargo build), запустити (cargo run) і написати щонайменше один змістовний юніт-тест, що перевіряє реальну поведінку функції (наприклад, детермінованість результату), запущений через cargo test. Сигнатура fn generate_key(seed: &str) -> String має лишитись незмінною.

РІВЕНЬ 2 · «4» · МАГІСТР

Рівень 2 (оцінка 4, захист)

Підключити сторонній crate для генерації хешів або шифрування (наприклад, sha2) у секцію [dependencies] файлу Cargo.toml і фактично використати його (use <crate>::...) усередині generate_key. На захисті — усно пояснити призначення обраної залежності та принцип роботи використаного алгоритму на високому рівні (наприклад, чому SHA-256 детермінований і односторонній).

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

Рівень 3 (оцінка 5, розширення)

Написати комплексний тест із моком (підміною) вхідних даних від "корпоративного сервера": визначити trait DataSource із методом на кшталт fn fetch(&self) -> String, реалізувати його для реального (чи заглушкового) джерела і окремо — мок-структурою в тестовому модулі, що повертає фіксоване значення без будь-якого реального звернення до мережі чи диска. Тест має покривати і успішний сценарій (мок повертає коректні дані), і хоча б один граничний/помилковий (наприклад, порожній рядок від джерела).

🧪 РОЗІБРАНИЙ ПРИКЛАД

Розібраний приклад

Розберемо, чим змістовний тест відрізняється від формального. Реалізація:

fn generate_key(seed: &str) -> String {
    let mut acc: u32 = 0;
    for (i, b) in seed.bytes().enumerate() {
        acc = acc.wrapping_add((b as u32).wrapping_mul(i as u32 + 1));
    }
    format!("{:08X}", acc)
}

Три тести, що реально перевіряють різні властивості:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generate_key_is_deterministic() {
        assert_eq!(generate_key("alpha"), generate_key("alpha"));
    }

    #[test]
    fn generate_key_differs_for_different_seed() {
        assert_ne!(generate_key("alpha"), generate_key("beta"));
    }

    #[test]
    fn generate_key_handles_empty_seed_without_panicking() {
        assert_eq!(generate_key(""), "00000000");
    }
}

Кожен тест ловить окремий клас регресій: перший — випадкову недетермінованість (наприклад, випадкове звернення до SystemTime::now()); другий — ігнорування вхідного параметра; третій — паніку на крайовому випадку порожнього вводу. Формальний assert!(true) не ловить жодної з цих регресій і саме тому відхиляється автогрейдером.

Корисні команди під час розробки:

cargo test generate_key_is_deterministic   # запустити один конкретний тест
cargo test -- --show-output                 # показати println! навіть при успіху
cargo build --release                       # оптимізована збірка без символів налагодження
❓ КОНТРОЛЬНІ ПИТАННЯ

Контрольні питання

  1. Яка різниця між cargo build і cargo run?
  2. Що робить атрибут #[cfg(test)] і чому тестовий код не потрапляє у фінальний бінарник?
  3. Чим cargo build --release відрізняється від звичайного cargo build?
  4. Що таке семантичне версіонування і як Cargo використовує його при виборі версії залежності?
  5. Навіщо потрібен мок, якщо можна протестувати код напряму проти реального сервера?
  6. Який макрос використати, щоб тест провалився з повідомленням, якщо два значення не рівні?
  7. Де Cargo зберігає завантажені залежності і що робить файл Cargo.lock?
  8. Чим trait відрізняється від конкретного типу і чому це дозволяє підміняти реалізацію в тесті?

Парні теми СРС

Критерії оцінювання та форма звіту

Студент здає лабораторну тегом submit/lab04 у своєму репозиторії/форку. Автоматичні тести рівня 1 (autograder/) перевіряють: проєкт збирається без помилок (cargo build), проєкт запускається (cargo run) і виводить очікуваний результат, cargo test завершується успішно й містить принаймні один тест, що реально перевіряє логіку (не порожній assert!(true)), а сигнатура fn generate_key(seed: &str) -> String лишилась незмінною.

Для рівня 2 автотести додатково перевіряють наявність зовнішньої залежності в Cargo.toml і її фактичне використання в коді (а не просто оголошення). Захист рівня 2 — усне пояснення, навіщо потрібна саме ця залежність і як працює обраний алгоритм хешування/шифрування на високому рівні.

Рівень 3 оцінюється вручну викладачем: наявність трейта для джерела даних, мок-реалізації в тестовому модулі й тесту, що перевіряє поведінку логіки на підставних даних без звернення до реального джерела. Оцінюється якість покриття (чи мок покриває і успішний, і помилковий сценарій).

Форма звіту: короткий опис реалізації (3–5 речень), скріншот терміналу із зеленим виводом cargo test, для рівня 2 — фрагмент Cargo.toml із підключеною залежністю, для рівня 3 — фрагмент коду мок-реалізації з коментарем, який сценарій вона перевіряє.

⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 7

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
rustc
Компілятор Rust; напряму викликається рідко (rustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.
rustup
Встановлювач і менеджер версій Rust: тулчейни, цілі (targets), компоненти.
cargo new
Створює новий проєкт із Cargo.toml і src/main.rs (cargo new lockpick; --lib — бібліотека).
cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.