Unit-тести в Anchor — це інструмент перевірки ізольованої логіки вашої програми без залежності від зовнішніх мережевих умов. Нижче наведено практичний підхід до організації тестів, який можна адаптувати під конкретну програму та інтегрувати в CI-конвеєр.

Структура тестового файлу в Anchor

Тестові файли розташовуються в директорії tests/ проєкту. Anchor використовує Mocha як тест-ранер і Chai для перевірок. Базовий файл тесту містить ініціалізацію провайдера, завантаження програми та набір тест-кейсів.

Передумови: встановлений Anchor CLI, зібрана програма (anchor build), Node.js 18+ із залежностями з package.json. Очікуваний результат — усі тести проходять локально без підключення до devnet чи mainnet.

Типова структура файлу tests/my-program.ts:

  • Імпорт залежностей та IDL програми.
  • Опис типів облікових записів (згенеровані з IDL або ручні).
  • Блок describe — логічна група тестів (наприклад, за інструкцією).
  • Блок before або beforeEach — спільна підготовка середовища.
  • Індивідуальні блоки it — конкретні тест-кейси.

Провайдер ініціалізується один раз на файл. Повторна ініціалізація в кожному тесті створює зайве навантаження та може призвести до невідтворюваних результатів через стан локального леджера.

Моки та фікстури для облікових записів

У контексті Solana-тестування «мокування» відрізняється від традиційного: ви не підміняєте облікові записи, а створюєте реальні облікові записи в локальному леджері з контрольованим станом. Це фікстури — підготовлені дані, які використовуються в кількох тестах.

Типова фікстура включає:

  • Ключову пару підписанта (payer) із sufficient балансом SOL для оренди облікових записів і комісій.
  • Попередньо ініціалізовані PDA-облікові записи програми з відомим станом.
  • Облікові записи токенів (Mint, Token Account), якщо програма працює з SPL Token.

Для створення PDA-фіксатури обчислюйте адресу через PublicKey.findProgramAddressSync із тими самими сидами, що й у програмі. Якщо сиди відрізняються від тих, що використовуються в Rust-коді, тест пройде, але це не перевірить реальну логіку — це поширена помилка, яка дає хибне відчуття покриття.

Баланс payer-акаунту поповнюється через airdrop у локальному середовищі. Не покладайтеся на залишковий баланс від попередніх тестів: beforeEach має гарантувати достатній баланс незалежно від порядку виконання.

Перевірка стану після виклику

Після виклику інструкції перевірка полягає у завантаженні облікового запису за адресою та порівнянні його полів із очікуваними значеннями. Використовуйте типізований метод program.account.[type].fetch(address), який повертає десеріалізовану структуру.

Перевіряйте лише ті поля, які змінює конкретна інструкція. Перевірка всіх полів облікового запису створює крихкі тести: будь-яка зміна в структурі, навіть не пов’язана з логікою інструкції, зламає тест без реальної причини.

Для числових полів використовуйте точне порівняння (.to.equal()), а не наближене. Anchor десеріалізує дані точно, тому наближене порівняння приховує реальні баги округлення.

Тестування помилкових сценаріїв

Перевірка того, що програма коректно відхиляє некоректні виклики, не менш важлива, ніж перевірка успішного шляху. Без цього ви тестуєте лише половину логіки.

В Anchor помилки інструкцій повертаються як об’єкти з полем error, що містить код та ім’я помилки з IDL. Базовий патерн перевірки:

  • Обернути виклик інструкції в try/catch або використати assert.rejects.
  • Перевірити, що виняток дійсно стався (без цього тест пройде навіть якщо інструкція раптово почне успішно виконуватись).
  • Перевірити конкретний код помилки або її ім’я з IDL.

Типові помилкові сценарії, які варто покрити:

  • Виклик від неавторизованого підписанта (authority mismatch).
  • Передача облікового запису з неправильним типом власника.
  • Спроба повторної ініціалізації вже ініціалізованого облікового запису.
  • Невідповідність сидів PDA (неправильний seeds передано в інструкцію).
  • Арифметичне переповнення або недостатній баланс для операції.

Помилка AccountNotInitialized виникає, коли програма намагається десеріалізувати обліковий запис, який ще не був ініціалізований. Якщо ви очікуєте цю помилку в тесті, переконайтеся, що обліковий запис дійсно не ініціалізовано в цьому тест-кейсі, а не успадковує стан від попереднього тесту через спільний леджер.

Не перевіряйте помилки за рядковим повідомленням — воно може змінюватися між версіями Anchor. Використовуйте код помилки або ім’я з error-enum у IDL.

Використання banks-client для локальних тестів

Крім стандартного провайдера Anchor, існує BanksClient з пакета @solana/program-test — нижчорівневий інтерфейс для взаємодії з локальним леджером. Він дає більше контролю над часом слоту, підписами та транзакціями.

Передумови: залежність @solana/program-test у проєкті, розуміння структури Transaction та Message у Solana. Ризик: banks-client не підтримує деякі високорівневі абстракції Anchor (наприклад, автоматичний resolution CPI-облікових записів), тому їх потрібно формувати вручну.

Коли banks-client доречний:

  • Потрібно контролювати час слота (наприклад, тестування Clock-залежної логіки).
  • Тестування поведінки при специфічних станах леджера (експірований blockhash — див. окремий матеріал про blockhash expired).
  • Потрібно надіслати транзакцію з нестандартною структурою підписів.
  • Інтеграційне тестування кількох програм, де Anchor-провайдер створює зайву абстракцію.

Базовий порядок роботи з banks-client:

  1. Створити ProgramTest та додати програму через .addProgram() із шляхом до .so файлу.
  2. Запустити тестове середовище через .start(), отримати BanksClient та PublicKey payer-а.
  3. Сформувати транзакцію вручну (Instruction, Transaction, підписи).
  4. Відправити через banksClient.processTransaction().
  5. Перевірити стан через banksClient.getAccount() та ручну десеріалізацію.

Відкат: якщо banks-client ускладнює підтримку тестів без очевидної вигоди, поверніться до стандартного Anchor-провайдера. Перехід не вимагає зміни програми, лише тестового коду.

Обмеження: banks-client не імітує мережеву затримку, поведінку інших валідаторів та реальну пропагацию транзакцій. Тести, що проходять у banks-client, можуть поводитись інакше на devnet через timing-відмінності.

Покриття кордонних випадків

Кордонні випадки (edge cases) — це вхідні дані або стани, які лежать на межі допустимих значень. У Solana-програмах вони часто пов’язані з обмеженнями розміру облікових записів, арифметикою та лічильниками.

Ключові кордонні випадки для Anchor-програм:

  • Нульові значення. Передача нульової суми в трансфер, нульового лічильника, порожнього рядку (якщо програма працює з рядковими даними через байтові масиви).
  • Максимальні значення. Перевірка поведінки при значеннях, близьких до u64::MAX, особливо в арифметичних операціях. Anchor перевіряє переповнення за замовчуванням, але варто переконатися, що помилка обробляється коректно, а не панікує.
  • Розмір облікового запису. Спроба записати дані, які перевищують зарезервований простір (space). Це має призводити до помилки запису, а не до тихої поразки з пошкодженням даних.
  • Закриття та повторне відкриття. Закриття облікового запису (instruction Close) з подальшою спробою звернутися до нього або повторно ініціалізувати.
  • Конкурентні транзакції. Дві транзакції, які одночасно намагаються змінити один обліковий запис. У локальному тестуванні це моделюється послідовною відправкою, але варто перевірити, що друга транзакція отримує очікувану помилку (застарілий лічильник або невідповідність даних).
  • Порожні вектори та масиви. Передача порожнього масиву облікових записів туди, де програма очікує непорожній.

Для кожного кордонного випадку формулюйте гіпотезу: «при значенні X програма має повернути помилку Y». Тест без чіткої гіпотези — це просто виклик коду, а не перевірка.

Спосіб перевірки покриття: після написання тестів запустіть anchor test --verbose та проаналізуйте логи на наявність неперевірених гілок. Для глибшого аналізу покриття коду Rust можна використовувати інструменти профілювання (наприклад, cargo-llvm-cov), але це вимагає додаткової конфігурації та не інтегровано в Anchor нативно.

Наступний логічний крок після налаштування unit-тестів — підготовка ізольованого локального середовища для стабільного запуску. Дивіться окремий матеріал про те, як налаштувати local validator для тестування.

Джерела