Tests et débogage#
Tester un programme Solana n’est pas une tache ordinaire. Contrairement a un binaire classique que l’on execute localement, un programme on-chain vit dans un environnement singulier : il est deploye sur un reseau de validateurs, invoque par des transactions signees, et contraint par un budget de compute units. Les outils de test doivent donc reproduire cet environnement avec suffisamment de fidelite pour que les bugs soient detectes avant le deploiement, tout en restant assez rapides pour s’integrer dans une boucle de developpement iterative.
L’ecosysteme Solana propose trois niveaux de test complementaires.
Le premier, et le plus courant avec Anchor, repose sur des tests TypeScript qui generent automatiquement un client type a partir de l’IDL du programme.
Le deuxieme utilise le crate Rust solana-program-test, qui instancie un validateur leger en memoire pour des tests unitaires rapides.
Le troisieme lance un validateur local complet (solana-test-validator) capable de cloner des comptes depuis le mainnet.
A ces trois niveaux s’ajoutent des outils de debogage — la macro msg!(), les logs en temps reel, et les evenements Anchor — qui permettent d’inspecter le comportement d’un programme a chaque etape de son execution.
Ce chapitre parcourt methodiquement ces outils.
Le lecteur, familier avec les tests en Rust grace au chapitre correspondant du Rust Book, retrouvera ici des concepts connus (#[test], assertions, gestion des erreurs) transposes dans le contexte specifique de Solana.
L’accent est mis sur ce qui differe : la generation automatique de clients TypeScript, le BanksClient, le validateur local, et les contraintes propres aux programmes on-chain.
Tests TypeScript avec Anchor#
Définition 97 (Framework de test Anchor)
Le framework de test Anchor repose sur Mocha (framework de test JavaScript) et Chai (bibliotheque d’assertions).
Lors de anchor build, Anchor genere un fichier IDL (Interface Description Language) au format JSON qui decrit les instructions, comptes et types du programme.
A partir de cet IDL, la bibliotheque @coral-xyz/anchor construit dynamiquement un client TypeScript type : chaque instruction du programme devient une methode appelable avec autocompletion et verification de types.
Les tests sont places dans le repertoire tests/ et executes par anchor test, qui :
Compile le programme (
anchor build).Demarre un validateur local.
Deploie le programme sur ce validateur.
Execute les tests Mocha.
Arrete le validateur.
Voici un fichier de test complet pour le programme de compteur du chapitre 8.
Ce programme expose deux instructions : initialize (cree un compteur a 0) et increment (incremente de 1, uniquement par l’autorite).
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Counter } from "../target/types/counter";
import { assert } from "chai";
import { Keypair, SystemProgram } from "@solana/web3.js";
describe("counter", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Counter as Program<Counter>;
const counter = Keypair.generate();
it("initialise le compteur a zero", async () => {
await program.methods
.initialize()
.accounts({
counter: counter.publicKey,
authority: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([counter])
.rpc();
const account = await program.account.counter.fetch(counter.publicKey);
assert.equal(account.count.toNumber(), 0);
assert.ok(account.authority.equals(provider.wallet.publicKey));
});
it("incremente le compteur de 1", async () => {
await program.methods
.increment()
.accounts({
counter: counter.publicKey,
authority: provider.wallet.publicKey,
})
.rpc();
const account = await program.account.counter.fetch(counter.publicKey);
assert.equal(account.count.toNumber(), 1);
});
it("echoue si l'autorite est incorrecte", async () => {
const fakeAuthority = Keypair.generate();
try {
await program.methods
.increment()
.accounts({
counter: counter.publicKey,
authority: fakeAuthority.publicKey,
})
.signers([fakeAuthority])
.rpc();
assert.fail("La transaction aurait du echouer");
} catch (err) {
assert.equal(err.error.errorCode.code, "ConstraintHasOne");
}
});
});
Remarque 62 (Le mecanisme anchor.workspace)
L’objet anchor.workspace est le point d’entree vers tous les programmes du projet.
Anchor decouvre automatiquement les programmes declares dans Anchor.toml, charge leur IDL, et genere un client type pour chacun.
Ainsi, anchor.workspace.Counter donne acces a un objet Program<Counter> dont les methodes correspondent exactement aux instructions definies dans le programme Rust.
Ce mecanisme elimine la serialisation manuelle des instructions et des comptes : le client sait quels comptes chaque instruction attend, quels sont leurs types, et quels signataires sont requis.
Exemple 29 (Envoyer une transaction avec le client Anchor)
La syntaxe chainee du client Anchor suit un schema constant :
const txSignature = await program.methods
.nomDeLInstruction(arg1, arg2) // Instruction et arguments
.accounts({ // Comptes requis
compte1: pubkey1,
compte2: pubkey2,
systemProgram: SystemProgram.programId,
})
.signers([keypair1, keypair2]) // Signataires additionnels
.rpc(); // Envoyer la transaction
La methode .rpc() envoie la transaction et retourne la signature.
Alternativement, .transaction() construit l’objet Transaction sans l’envoyer, et .instruction() retourne l’instruction Solana brute.
Remarque 63 (Options de confirmation et niveaux de commitment)
Lorsqu’une transaction est envoyee, elle passe par trois niveaux de confirmation :
processed : traitee par le leader actuel, pas encore confirmee par le cluster. Rapide mais la transaction peut etre annulee.
confirmed : confirmee par une super-majorite de validateurs. Niveau par defaut, recommande pour les tests.
finalized : inscrite dans un bloc ayant atteint la finalite maximale (~32 slots). Le plus sur, mais le plus lent.
const provider = new anchor.AnchorProvider(
connection, wallet,
{ commitment: "confirmed", preflightCommitment: "confirmed" }
);
Pour les tests locaux, confirmed suffit. En production, les operations critiques devraient attendre finalized.
Tests en Rust avec solana-program-test#
Définition 98 (solana-program-test et BanksClient)
Le crate solana-program-test fournit un validateur leger en memoire pour tester les programmes Solana en Rust.
Son composant central est le BanksClient, une interface qui emule un validateur complet sans processus externe.
L’initialisation suit trois etapes :
Creer un
ProgramTesten specifiant le nom du programme et sonprocess_instruction.Appeler
.start()pour obtenir un triplet(BanksClient, Keypair, Hash)— le client, le payeur, et le hash recent.Construire des transactions, les envoyer via
banks_client.process_transaction(), et inspecter les comptes.
Le BanksClient s’execute en quelques millisecondes, ce qui le rend ideal pour les tests unitaires rapides.
#[cfg(test)]
mod tests {
use solana_program_test::*;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey, signature::Signer,
transaction::Transaction, system_instruction,
};
use borsh::BorshDeserialize;
#[derive(BorshDeserialize, Debug)]
struct CounterAccount {
pub authority: Pubkey,
pub count: u64,
}
#[tokio::test]
async fn test_initialize_counter() {
let program_id = Pubkey::new_unique();
let mut program_test = ProgramTest::new(
"counter", program_id,
processor!(process_instruction),
);
let (mut banks_client, payer, recent_blockhash) =
program_test.start().await;
let counter = solana_sdk::signature::Keypair::new();
let space = 8 + 32 + 8; // discriminateur + authority + count
let rent = banks_client.get_rent().await.unwrap()
.minimum_balance(space);
let create_ix = system_instruction::create_account(
&payer.pubkey(), &counter.pubkey(),
rent, space as u64, &program_id,
);
let init_ix = Instruction {
program_id,
accounts: vec![
AccountMeta::new(counter.pubkey(), false),
AccountMeta::new_readonly(payer.pubkey(), true),
AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
],
data: vec![0], // Discriminateur pour 'initialize'
};
let mut tx = Transaction::new_with_payer(
&[create_ix, init_ix], Some(&payer.pubkey()),
);
tx.sign(&[&payer, &counter], recent_blockhash);
banks_client.process_transaction(tx).await.unwrap();
let account = banks_client.get_account(counter.pubkey())
.await.unwrap().unwrap();
let data = CounterAccount::try_from_slice(&account.data[8..]).unwrap();
assert_eq!(data.count, 0);
assert_eq!(data.authority, payer.pubkey());
}
}
Remarque 64 (Avantages des tests Rust)
Les tests avec solana-program-test offrent plusieurs avantages :
Vitesse : le
BanksClients’execute dans le meme processus, sans validateur externe. Un test s’execute en quelques millisecondes.Acces direct : on peut tester des fonctions internes du programme sans passer par l’interface publique.
Integration
cargo test: les tests suivent les conventions Rust standard (parallelisme, filtrage,--nocapture).Controle fin : on peut injecter des comptes pre-configures, simuler l’avancement du temps (warp), ou forcer des etats specifiques.
Remarque 65 (Inconvenients et compromis)
En contrepartie, les tests Rust sont nettement plus verbeux.
Chaque instruction doit etre construite manuellement : specifier les AccountMeta, serialiser les donnees, calculer le loyer, creer les comptes.
Le client Anchor fait tout cela automatiquement a partir de l’IDL.
De plus, le BanksClient ne simule pas les latences, les reorganisations de blocs ni les conditions de concurrence.
En pratique, une strategie mixte est recommandee : tests Rust pour la logique unitaire critique, tests TypeScript pour les scenarios d’integration.
Le validateur local#
Définition 99 (solana-test-validator)
Le solana-test-validator est un validateur Solana complet qui s’execute localement.
Contrairement au BanksClient, c’est un processus independant qui expose une interface RPC standard sur http://localhost:8899.
Il supporte l’ensemble des fonctionnalites d’un validateur de production : traitement des transactions, avancement des slots, calcul du loyer, et execution des programmes.
Tout client Solana (CLI, SDK TypeScript, SDK Rust) peut s’y connecter comme a un cluster distant.
# Lancer le validateur (persistant entre les redemarrages)
solana-test-validator
# Repartir d'un etat vierge
solana-test-validator --reset
# Pre-charger un programme compile
solana-test-validator \
--bpf-program <PROGRAM_ID> ./target/deploy/counter.so --reset
# Cloner un compte depuis le mainnet
solana-test-validator \
--clone <ACCOUNT_ADDRESS> --url mainnet-beta --reset
# Cloner un programme entier depuis le mainnet
solana-test-validator \
--clone-upgradeable-program <PROGRAM_ID> --url mainnet-beta --reset
Exemple 30 (Cloner un pool Raydium depuis le mainnet)
Pour tester l’interaction avec un pool Raydium sans risquer de fonds :
RAYDIUM_PROGRAM=675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
POOL_STATE=58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2
solana-test-validator \
--clone-upgradeable-program $RAYDIUM_PROGRAM \
--clone $POOL_STATE \
--url mainnet-beta --reset
Le validateur local contient maintenant une copie exacte du programme Raydium et du compte du pool. On peut y envoyer des transactions de swap identiques a celles du mainnet, sans frais reels. Cette technique est essentielle pour le developpement de bots d’arbitrage et de protocoles DeFi.
Remarque 66 (anchor test et le validateur persistant)
La commande anchor test gere automatiquement le cycle de vie du validateur : demarrage, deploiement, execution des tests, arret.
C’est le flux recommande pour le developpement quotidien.
Cependant, un validateur persistant est preferable dans certains cas :
Tests iteratifs : eviter le temps de redemarrage avec
anchor test --skip-local-validator.Comptes clones : lancer le validateur une seule fois avec les options
--cloneadequates.Debogage interactif : inspecter l’etat des comptes entre les executions via
solana account <ADDRESS>.
Logs et debogage#
Solana ne propose pas de debugger interactif.
Le debogage repose sur la journalisation : on insere des messages dans le programme via la macro msg!(), et on les observe dans le flux de logs.
Définition 100 (La macro msg!())
La macro msg!() est la fonction de journalisation standard des programmes Solana.
Elle ecrit un message dans le journal de la transaction, visible via la commande solana logs ou dans les explorateurs de blocs.
Sa syntaxe est similaire a println!() en Rust, avec support du formatage :
msg!("Initialisation du compteur");
msg!("Autorite : {}", ctx.accounts.authority.key());
msg!("Increment: {} -> {}", old_count, new_count);
Les messages apparaissent dans les logs prefixes par Program log:.
use anchor_lang::prelude::*;
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Initialisation du compteur");
msg!("Autorite : {}", ctx.accounts.authority.key());
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
msg!("Compteur initialise, count = {}", counter.count);
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
let old_count = counter.count;
counter.count += 1;
msg!("Increment: {} -> {}", old_count, counter.count);
Ok(())
}
}
# Suivre tous les logs du cluster local
solana logs
# Filtrer par niveau de commitment
solana logs --commitment confirmed
# Filtrer par programme specifique
solana logs <PROGRAM_ID>
Exemple 31 (Scripts avec anchor run)
Anchor permet de definir des scripts personnalises dans Anchor.toml pour les taches repetitives :
# Dans Anchor.toml :
# [scripts]
# seed-data = "ts-node scripts/seed-data.ts"
# migrate = "ts-node scripts/migrate.ts"
anchor run seed-data
anchor run migrate
Ces scripts sont utiles pour le peuplement de comptes de test, les migrations de donnees, ou les etapes de deploiement automatisees.
Remarque 67 (msg!() et le budget de compute units)
Chaque appel a msg!() consomme des compute units (CU).
Une transaction dispose par defaut de 200 000 CU, et chaque msg!() en consomme entre 100 et plusieurs milliers selon la longueur du message.
En developpement, la journalisation detaillee est precieuse.
En production, un programme trop bavard peut epuiser son budget et provoquer l’echec de la transaction.
La bonne pratique : msg!() genereux en developpement, strict minimum en production, eventuellement derriere un feature flag (#[cfg(feature = "verbose")]).
Remarque 68 (Evenements Anchor)
Pour un logging structure, Anchor propose les evenements via #[event].
Un evenement est une structure Rust serialisee dans les logs, capturable et decodable par les clients.
#[event]
pub struct CounterIncremented {
pub authority: Pubkey,
pub old_count: u64,
pub new_count: u64,
}
// Dans l'instruction :
emit!(CounterIncremented {
authority: ctx.accounts.authority.key(),
old_count: old_count,
new_count: counter.count,
});
Cote TypeScript :
const listener = program.addEventListener(
"counterIncremented",
(event, slot) => {
console.log(`Increment: ${event.oldCount} -> ${event.newCount}`);
}
);
program.removeEventListener(listener);
Les evenements consomment des CU mais offrent un format parsable par les clients, les indexeurs et les interfaces utilisateur.
Patterns de test#
Définition 101 (Les trois niveaux de test)
Les tests d’un programme Solana se decomposent en trois niveaux :
Test du chemin nominal (happy path) : verifier que chaque instruction fonctionne correctement dans des conditions normales. C’est le premier test a ecrire.
Test des cas d’erreur (error testing) : verifier que les operations invalides echouent avec l’erreur attendue. Un programme qui accepte des entrees invalides est un programme vulnerable.
Test d’integration : verifier l’interaction entre plusieurs instructions et la coherence de l’etat global apres une sequence d’operations.
Le fichier de test du compteur presente plus haut illustre ces trois niveaux : le premier it() est un happy path, le deuxieme un test d’integration (il depend de l’initialisation), et le troisieme un test d’erreur.
Exemple 32 (Test d’integration avec sequence d’operations)
Un test d’integration typique enchaine plusieurs instructions et verifie la coherence finale :
it("supporte une sequence complete d'operations", async () => {
const newCounter = Keypair.generate();
await program.methods.initialize()
.accounts({
counter: newCounter.publicKey,
authority: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([newCounter]).rpc();
for (let i = 0; i < 3; i++) {
await program.methods.increment()
.accounts({
counter: newCounter.publicKey,
authority: provider.wallet.publicKey,
}).rpc();
}
const account = await program.account.counter.fetch(newCounter.publicKey);
assert.equal(account.count.toNumber(), 3);
});
Ce test cree un compteur isole, l’incremente trois fois, puis verifie que l’etat final est coherent.
L’isolation (nouveau Keypair a chaque test) garantit l’independance vis-a-vis des autres tests.
Cycle de developpement#
La visualisation ci-dessous represente le cycle iteratif du developpement d’un programme Solana.
Remarque 69 (Bonnes pratiques de test)
Toujours tester les cas d’erreur. Un programme Solana est un contrat financier : une instruction qui accepte des entrees invalides peut entrainer des pertes irreversibles.
Tester avec differents signataires. Les bugs de controle d’acces sont parmi les plus courants. Verifier systematiquement qu’un utilisateur non autorise ne peut pas invoquer une instruction protegee.
Tester les cas limites. Que se passe-t-il si un montant vaut zero ? Si un compteur atteint
u64::MAX? Si un compte est deja initialise ?Isoler chaque test. Creer des comptes dedies par test pour eviter les dependances sur l’etat laisse par un test precedent.
Strategie mixte.
anchor testpour le flux quotidien,solana-program-testpour les tests unitaires critiques, validateur local avec--clonepour les tests d’integration avec des programmes externes.
Resume#
Concept |
Description |
|---|---|
Framework de test Anchor |
Mocha + Chai, client TypeScript type genere depuis l’IDL |
anchor.workspace |
Decouverte automatique des programmes, generation de clients types |
Commitment levels |
|
solana-program-test |
Crate Rust avec |
BanksClient |
Interface emulant un validateur complet, sans processus externe |
solana-test-validator |
Validateur local complet avec interface RPC standard |
–clone |
Copier des comptes depuis le mainnet ou le devnet vers le validateur local |
msg!() |
Macro de journalisation, visible dans |
Compute units |
Budget par transaction (200 000 CU par defaut), consomme par chaque operation |
Evenements Anchor |
Logging structure via |
Happy path |
Verifier le comportement attendu en conditions normales |
Error testing |
Verifier que les operations invalides echouent avec l’erreur attendue |
Integration testing |
Tester l’interaction entre plusieurs instructions |
Le chapitre suivant introduira les tokens SPL — le standard de jetons fongibles et non fongibles de Solana — et montrera comment les creer, transferer et gerer depuis un programme Anchor.