Банківські тести (banks-server) — це вбудований у Solana SDK механізм інтеграційного тестування, який запускає спрощений in-memory екземпляр рантайму Solana в процесі вашого тесту. На відміну від unit-тестів, де ви мокаєте рантайм, banks-server виконує програми через справжній BanksEngine — з реальними обчисленнями, перевірками прав доступу та повним циклом обробки транзакцій. Нижче — покрокова інструкція з налаштування, архітектурні нюанси та перевірені патерни для production-готового тестування.
Архітектура banks-server для інтеграційного тестування
Розуміння внутрішньої архітектури banks-server необхідне, щоб передбачити поведінку тестів і правильно інтерпретувати помилки.
Основа — crate solana-program-test, який надає структуру ProgramTest. При ініціалізації ProgramTest створює три ключові компоненти:
- BanksServer — in-memory реалізація банківського інтерфейсу Solana. Приймає серіалізовані транзакції та проганяє їх через справжній BanksEngine (той самий, що працює у валідаторі).
- BanksClient — клієнтська обгортка над gRPC-з'єднанням до BanksServer. Надає методи process_transaction, get_account, get_balance та інші для взаємодії з рантаймом.
- Genesis — початковий стан леджера: системні програми, вказані користувацькі програми, попередньо створені акаунти та значення sysvar.
Життєвий цикл тесту:
- Ви описуєте ProgramTest::new(), вказуючи шлях до скомпільованої програми (.so файл) та початкові акаунти.
- Викликаєте .start() — це підіймає BanksServer у фоновому потоці та повертає BanksClient разом із Payer (ключова пара, що фінансує транзакції).
- Через BanksClient відправляєте транзакції та читаєте стан акаунтів.
- Після завершення тесту сервер зупиняється автоматично при drop.
Що banks-server НЕ робить
Це критично важливо для коректного планування тестів:
- Не запускає консенсус — немає слотів, лідерів чи епох у повному сенсі.
- Не виконує PoH (Proof of History) — замість нього використовується спрощена генерація хешів.
- Не підтримує мережевий рівень — неможливо тестувати взаємодію між кількома валідаторами.
- Не імітує реальну затримку мережі чи пакетну обробку транзакцій.
Якщо вам потрібне тестування з консенсусом чи мережевою поведінкою — це завдання для localnet (запущеного локально валідатора), а не для banks-server.
Передумови та початкове налаштування
Середовище: Rust toolchain, зібраний проєкт на Anchor або чистому Rust, наявність скомпільованої програми.
Очікуваний результат: тест компілюється, запускається, BanksServer підіймається, транзакція обробляється, стан акаунта змінюється.
У Cargo.toml вашого тестового crate додайте:
[dev-dependencies]
solana-program-test = "<перевірте актуальну версію у репозиторії Solana>"
solana-sdk = "<перевірте актуальну версію>"
solana-banks-client = "<перевірте актуальну версію>"
Версії всіх трьох crate мають збігатися — несумісність версій є найчастішою причиною падінь тестів на етапі серіалізації транзакцій.
Базовий каркас тесту:
#[tokio::test]
async fn basic_integration_test() {
let mut program_test = ProgramTest::new(
"my_program",
id(),
processor!(my_program::entry),
);
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
}
Увага: наведений код є ілюстративним прикладом. Перед використанням у вашому проєкті перевірте відповідність сигнатур поточній версії solana-program-test.
Множинні транзакції в одному тесті
Реальні сценарії використання майже завжди вимагають кількох транзакцій: ініціалізація акаунта, оновлення стану, закриття. У banks-server це працює, але має специфіку, яку треба розуміти.
Послідовні транзакції
Найпростіший випадок — відправка транзакцій одна за одною. Після кожного виклику process_transaction BanksServer обробляє транзакцію і оновлює стан. Наступна транзакція побачить оновлений стан.
Нюанс із recent_blockhash: у справжній мережі blockhash змінюється кожен слот. У banks-server він залишається стабільним протягом життя тесту, якщо ви не зміните його вручну через set_sysvar. Тому ви можете безпечно використовувати один і той самий blockhash для всіх транзакцій у тесті.
Паралельні транзакції та nonce-акаунти
Якщо вам потрібно відправити кілька транзакцій, які залежать одна від одної в межах одного блоку (наприклад, арбітражна логіка), використовуйте nonce-акаунти (durable nonces). Без них banks-server обробляє транзакції послідовно навіть якщо ви викликаєте process_transaction з різних tokio-завдань — це не імітує реальне паралельне виконання.
Патерн роботи з nonce:
- Створіть nonce-акаунт через системну інструкцію CreateNonceAccount.
- У кожній транзакції вкажіть цей nonce-акаунт у полі nonce замість recent_blockhash.
- Додайте інструкцію AdvanceNonceAccount як першу інструкцію в кожній транзакції.
Це гарантує визначений порядок обробки навіть за умови конкурентного відправлення.
Типова помилка: застарілі дані акаунта
Якщо ви читаєте стан акаунта через get_account до завершення process_transaction, ви отримаєте застарілі дані. Завжди .await результат process_transaction перед читанням. Це очевидно, але стає причиною хибних тестів при рефакторингу асинхронного коду — компілятор не попереджає про таку помилку, оскільки типи залишаються коректними.
Перевірка міжпрограмної взаємодії через CPI
Banks-server — єдиний зручний спосіб перевірити CPI (Cross-Program Invocation) на локальному рівні без розгортання в testnet. Рантайм обробляє CPI-виклики нативно: викликається invoke або invoke_signed, рантайм завантажує цільову програму та виконує її інструкції з правильним контекстом.
Розгортання кількох програм
Для тестування CPI вам потрібно додати всі залучені програми до ProgramTest:
let mut program_test = ProgramTest::new(
"program_a",
program_a_id(),
processor!(program_a::entry),
);
program_test.add_program(
"program_b",
program_b_id(),
processor!(program_b::entry),
);
Порядок додавання не має значення — всі програми стають доступними для CPI одразу після .start().
Перевірка PDA при CPI з invoke_signed
Найскладніший випадок CPI — коли програма A викликає програму B, підписуючи PDA (Program Derived Address). У banks-server це працює коректно, але помилки в насінні PDA виявляються саме тут, а не в unit-тестах, де рантайм мокований.
Критерій перевірки: після CPI-виклику прочитайте стан акаунта, який мав бути модифікований програмою B. Якщо PDA-підпис некоректний, рантайм поверне помилку на рівні виклику invoke_signed — і ви побачите її як результат process_transaction.
Не намагайтеся «перевірити підпис PDA окремо» — у Solana немає такого механізму. Єдиний спосіб — виконати транзакцію і перевірити кінцевий стан.
Обмеження: системні програми
Banks-server включає системні програми (System, Token, Associated Token Account), але їхні версії фіксуються разом із версією SDK. Якщо ваша програма покладається на поведінку конкретної версії Token Program (наприклад, Token-2022), переконайтеся, що версія solana-program-test відповідає версії, з якою ви деплоїте в production. Перевірте це у репозиторії Solana перед написанням тестів — розбіжність версій Token Program є причиною помилок, які відсутні в testnet але проявляються в banks-server (і навпаки).
Тестування часових залежностей
Багато програм Solana залежать від часу: vesting-контракти, lockup-періоди, аукціони з таймаутом, стейкінг із cooldown. У banks-server час керується через sysvar Clock, і ви маєте повний контроль над ним.
Зсув часу через set_sysvar
BanksClient надає метод set_sysvar, який дозволяє прямо записати нове значення будь-якого sysvar, включно з Clock:
let mut clock = banks_client.get_sysvar::<Clock>().await.unwrap();
clock.unix_timestamp += 86400; // +1 доба
banks_client.set_sysvar(&clock).await;
Після цього будь-яка транзакція, яка читає Clock::get().unix_timestamp, побачить зсунуте значення. Це дозволяє протестувати весь життєвий цикл тимчасової логіки в одному тесті без реального очікування.
Перевірка кордонних значень
Для часових залежностей найважливіший клас помилок — off-by-one на кордонах. Обов'язково покрийте тестами такі точки:
- Миттєве виконання — транзакція одразу після ініціалізації (час не змінено).
- Остання заборонена секунда — час встановлено на deadline - 1, транзакція має бути відхилена.
- Перша дозволена секунда — час встановлено на deadline, транзакція має пройти.
- Значне перевищення — час встановлено на deadline + 1_000_000, перевірте, що логіка не ламається на великих значеннях (переповнення при обчисленнях різниці).
Обмеження: Slot та Epoch
Clock містить не лише unix_timestamp, але й slot, epoch_start_timestamp, epoch. У banks-server slot не інкрементується автоматично при обробці транзакцій. Якщо ваша програма перевіряє slot (наприклад, для визначення епохи), вам потрібно вручну оновлювати всі поля Clock синхронно. Інакше ви отримаєте несумісний стан, який неможливий у реальній мережі, і ваш тест буде перевіряти хибний сценарій.
План відкату при проблемах із тестами
Якщо інтеграційні тести з banks-server починають нестабільно падати після оновлення залежностей:
- Зафіксуйте версії solana-program-test, solana-sdk та solana-banks-client на останній стабільній комбінації в Cargo.lock.
- Перевірте структуру sysvar — зміна полів Clock чи інших sysvar між мінорними релізами SDK є частою причиною серіалізаційних помилок.
- Ізолюйте CPI-проблеми — якщо падіння пов'язане з CPI, створіть мінімальний тест із двома програмами і перевірте, чи відтворюється помилка без бізнес-логіки.
- Перевірте issue-трекер Solana — якщо відкат залежностей неможливий (наприклад, потрібна нова фіча SDK), створіть мінімальний репродуктор і перевірте, чи це відомий регрес.
Наступний логічний крок після налаштування стабільних інтеграційних тестів — діагностика конкретних помилок рантайму, з якими ви зіткнетеся під час їх виконання. Якщо тест падає з InstructionError, перейдіть до матеріалу про діагностику цієї помилки у відповідному розділі.