Локальне тестування в Anchor — це запуск вбудованого локального validator'а, автоматичне розгортання програми та виконання інтеграційних тестів на TypeScript у межах одного кластера на вашій машині. Вам не потрібен Devnet, не потрібні реальні токени й не потрібне зовнішнє підключення. Усе працює ізольовано, швидко та передбачувано.

Версії для прикладів у «Як тестувати просту програму локально» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування.

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

Середовище: Anchor 0.30.x, Solana CLI 1.18.x, Rust stable, Node.js 18+, macOS або Linux (Ubuntu 24.04 LTS+).
Кластер: localnet (вбудований у Anchor).
Передумови: готовий Anchor-проєкт із скомпільованою програмою та визначеними accounts (див. Anchor accounts: визначення та зв'язування даних).

Налаштування тестового середовища

Конфігурація Anchor.toml для тестів

Файл Anchor.toml у корені проєкту керує тим, як програма розгортається та які параметри передаються тестам. Для локального тестування ключовий розділ — [programs.localnet]. Переконайтеся, що він містить правильний ідентифікатор програми:

Параметр Призначення
[programs.localnet] Вказує Anchor, який program ID використовувати при розгортанні на локальному кластері
[test] Налаштування запуску тестів (затримка перед стартом, додаткові скрипти)
[features] Увімкнення або вимкнення seed-перевірки для швидшої компіляції в розробці

Мінімальна робоча конфігурація:

[features]
seeds = false

[programs.localnet]
counter_program = "CnXeFgEh4JNL4rF4BZaHvD8xX8UoGKmYqLbFqRtS8vDp"

Значення counter_program та program ID мають збігатися з тим, що згенеровано у вашому проєкті. Перевірте файл target/idl/counter_program.json (поле address) або target/types/counter_program.ts. Якщо ви змінили ім'я програми при створенні проєкту через anchor init, використовуйте саме його.

Опція seeds = false у розділі [features] вимикає перевірку детермінованості seed-генерації під час компіляції. Це прискорює ітерації в розробці, але для production-збірки її слід увімкнути.

anchor test — запуск локального validator

Команда anchor test виконує три дії послідовно:

  1. Запускає локальний Solana validator у фоновому режимі (еквівалент solana-test-validator).
  2. Розгортає вашу програму на цьому локальному кластері.
  3. Запускає тести через ts-node з файлів у директорії tests/.

Перевірте, що базова команда спрацьовує без помилок:

anchor test

Очікуваний результат: у терміналі з'являється лог запуску validator'а, потім — результати тестів. Якщо тестів ще немає, ви побачите 0 passing без помилок компіляції. Це означає, що середовище налаштоване коректно.

Якщо вам потрібно лише запустити validator без тестів (наприклад, для ручної перевірки через solana CLI), використовуйте:

anchor localnet

Це корисно для налагодження, але для автоматизованої перевірки логіки програми використовуйте саме anchor test.

Написання інтеграційного тесту

Створення тестового файлу у tests/

Anchor автоматично знаходить усі файли з розширенням .ts у директорії tests/ і виконує їх як тестові сценарії. Створіть файл:

tests/counter.test.ts

Базова структура файлу:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CounterProgram } from "../target/types/counter_program";

describe("counter-program", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.CounterProgram as Program<CounterProgram>;

  it("Ініціалізує лічильник", async () => {
    // Викличте метод ініціалізації, а потім перевірте збережене значення через fetch
  });
});

Рядок anchor.setProvider(anchor.AnchorProvider.env()) читає конфігурацію з файлу ~/.config/solana/cli/config.yml і підключається до локального кластера. Під час виконання anchor test цей файл автоматично вказує на локальний validator.

Ініціалізація provider та program

Provider — це об'єкт, який поєднує connection (з'єднання з кластером), wallet (гаманець для підпису транзакцій) та опції (наприклад, commit level). У тестах ви зазвичай використовуєте wallet, який Anchor генерує автоматично при першому запуску anchor test. Цей гаманець отримує великий баланс SOL на локальному кластері, тому вам не потрібно турбуватися про оплату транзакцій під час тестування.

Program — це типізований клієнт, згенерований Anchor на основі IDL вашої програми. Він дозволяє викликати інструкції та читати стан акаунтів без ручного конструювання транзакцій. Тип CounterProgram імпортується з згенерованого файлу target/types/counter_program.ts.

Якщо при імпорті типу ви отримуєте помилку компіляції, переконайтеся, що програма була хоча б раз успішно зібрана через anchor build. Файли типів генеруються саме під час збірки.

Виклик instruction у тесті

Формування транзакції через згенерований клієнт

Anchor-клієнт автоматично конструює транзакцію на основі сигнатури інструкції з IDL. Вам не потрібно вручну серіалізувати дані, вказувати program ID або розраховувати розмір інструкції. Достатньо викликати метод з правильними параметрами.

Приклад виклику інструкції initialize, яка створює акаунт лічильника:

it("Ініціалізує лічильник", async () => {
  const counter = anchor.web3.Keypair.generate();

  await program.methods
    .initialize()
    .accounts({
      counter: counter.publicKey,
      user: program.provider.publicKey,
    })
    .signers([counter])
    .rpc();
});

Метод .rpc() відправляє транзакцію та чекає її підтвердження. Це зручна абстракція, яка підходить для більшості тестів. Якщо вам потрібен більший контроль (наприклад, додати кілька інструкцій в одну транзакцію), використовуйте .instruction() для отримання об'єкта TransactionInstruction та додавайте його до Transaction вручну.

Передача акаунтів та параметрів

Об'єкт, який передається в .accounts(), має точно збігатися зі структурою контексту інструкції у вашій програмі на Rust. Anchor під час виклику автоматично перевіряє відповідність імен та типів акаунтів з IDL.

Ключові правила:

  • signers — масив Keypair, які підписують транзакцію. Сюди входять акаунти, що ініціалізуються через init, та будь-які акаунти, де програма вимагає підпису (mut + signer).
  • Акаунти, що ініціалізуються через init, обов'язково мають бути передані як Keypair у .signers(), оскільки їхній publicKey потрібен до виконання транзакції, а privateKey — для підпису.
  • PDA (Program Derived Address) не потребують підпису, їхній адрес обчислюється через anchor.web3.PublicKey.findProgramAddressSync().
  • Параметри інструкції (числа, рядки тощо) передаються як аргументи в .methods.назваІнструкції(параметр1, параметр2).

Приклад виклику інструкції increment, яка збільшує значення лічильника, де акаунт лічильника є PDA:

const [counterPda, bump] = anchor.web3.PublicKey.findProgramAddressSync(
  [Buffer.from("counter"), program.provider.publicKey.toBuffer()],
  program.programId
);

await program.methods
  .increment()
  .accounts({
    counter: counterPda,
    user: program.provider.publicKey,
  })
  .rpc();

Сіди у findProgramAddressSync мають точно збігатися з тими, що використовуються у вашій програмі на Rust (у макросі #[account] з атрибутом seeds = [...]). Навіть незначна розбіжність (наприклад, різне кодування рядка) призведе до генерації іншого PDA, і тест завершиться помилкою.

Перевірка результатів

Читання стану акаунта після виклику

Після виклику інструкції вам потрібно підтвердити, що стан акаунта змінився очікуваним чином. Anchor-клієнт надає метод .fetch() для типізованого читання даних:

const counterAccount = await program.account.counter.fetch(counterPda);
console.log("Значення лічильника:", counterAccount.count.toString());

Для перевірки в тесті використовуйте твердження (assertions). У прикладах нижче використовується вбудований у Node.js модуль assert:

import assert from "assert";

// Після ініціалізації лічильник має бути 0
const account = await program.account.counter.fetch(counterPda);
assert.strictEqual(account.count.toNumber(), 0, "Лічильник має бути 0 після ініціалізації");

// Після increment лічильник має бути 1
await program.methods.increment().accounts({
  counter: counterPda,
  user: program.provider.publicKey,
}).rpc();

const updated = await program.account.counter.fetch(counterPda);
assert.strictEqual(updated.count.toNumber(), 1, "Лічильник має бути 1 після increment");

Метод .fetch() повертає типізований об'єкт, де поля відповідають структурі вашого акаунта на Rust. Числові типи (u8, u64, u128) представлені як BN (BigNumber) у JavaScript. Для порівняння з числом використовуйте .toNumber() або порівнюйте через .toString().

Перевірка помилок та edge cases

Тести мають перевіряти не лише щасливий шлях, а й ситуації, де програма має відхилити транзакцію. Для цього використовуйте assert.rejects:

await assert.rejects(
  program.methods
    .increment()
    .accounts({
      counter: wrongPda,
      user: program.provider.publicKey,
    })
    .rpc(),
  (err) => {
    assert(err instanceof anchor.AnchorError);
    // Перевірте конкретний код помилки у вашій версії Anchor
    // через err.error.errorCode.number та err.error.errorCode.name
    return true;
  }
);

Anchor-помилки мають тип AnchorError і містять структуровану інформацію: код помилки (errorCode.number), ім'я помилки (errorCode.name) та повідомлення. Коди помилок фіксовані та задаються макросами перевірки (ConstraintOwner, ConstraintSeeds, ConstraintMut тощо). Не покладайтеся на текст повідомлення у тестах — використовуйте числовий код. Перевірте актуальні коди у документації вашої версії Anchor.

Типові edge cases для перевірки:

  • Виклик інструкції з неправильним акаунтом-власником.
  • Повторна ініціалізація вже існуючого акаунта (має завершитися помилкою AccountAlreadyInUse або аналогічною).
  • Передача некоректних параметрів (негативні числа, занадто великі значення для типу).

Типові помилки

Програма не знайдена (program not found)

Помилка виглядає так:

Error: Program 11111111111111111111111111111111 not found

Причини та рішення:

  • Невідповідність program ID у Anchor.toml та в коді програми. Перевірте, що [programs.localnet] у Anchor.toml містить той самий ID, що й у файлі target/idl/counter_program.json (поле address), а також у декларації declare_id! у файлі programs/counter_program/src/lib.rs. Усі три місця мають збігатися.
  • Програма не була зібрана. Запустіть anchor build перед anchor test. Команда anchor test викликає збірку автоматично, але якщо ви змінили ID вручну без перезбірки, виникне розбіжність.
  • Залишок від попереднього запуску validator'а. Якщо ви запускали anchor localnet окремо і не зупинили його, anchor test може підключитися до старого процесу з іншим станом. Зупиніть усі процеси solana-test-validator перед тестуванням: pkill -f solana-test-validator.

Тест не чекає підтвердження транзакції

Іноді тест проходить, але наступне читання акаунта повертає застарілі дані. Це означає, що транзакція ще не була підтверджена на момент виклику .fetch().

Причина: за замовчуванням .rpc() використовує commit level processed, який не гарантує фіналізації. Для тестів надійніше вказати finalized безпосередньо у виклику:

await program.methods
  .increment()
  .accounts({
    counter: counterPda,
    user: program.provider.publicKey,
  })
  .rpc({ commitment: "finalized" });

Перевірте точну сигнатуру методу .rpc() у вашій версії @coral-xyz/anchor — підтримка параметра commitment залежить від версії пакета.

Для локального тестування різниця в швидкості між processed та finalized мінімальна, тому використання finalized у тестах — це безпечна практика, яка усуває цілий клас помилок, пов'язаних із таймінгом.

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

Джерела