NOUVEAU : Helius acquiert Light Protocol
Guide des tests de programmes Solana
Blog/Développement

Guide des tests de programmes Solana

Developer Experience Engineer0xIchigo sur X0xIchigo sur LinkedIn0xIchigo sur GitHub
27 min de lecture

Introduction

Dans un environnement blockchain, les tests dépassent le paradigme traditionnel des tests logiciels et introduisent des défis spécifiques aux conséquences plus importantes. Dans l’environnement à haut débit et à faible latence de Solana, la marge d’erreur est étroite. Les tests automatisés ne sont donc pas seulement une bonne pratique, mais une nécessité fondamentale pour garantir la fiabilité et la sécurité des programmes qui fonctionnent dans l’environnement dynamique et exigeant de Solana.

Cet article explore les principaux types de tests automatisés : tests unitaires, tests d’intégration et tests de bout en bout (E2E). Il explique également comment écrire des tests unitaires simples en JavaScript/TypeScript et en Rust, avant d’analyser les frameworks de test Solana les plus populaires. Enfin, il présente un exemple pratique qui teste un programme de jeu « King of the Hill ».

Si vous découvrez Solana, je vous recommande de commencer par lire ces précédents articles de blog :

Cet article complète également notre précédent article sur la sécurité des programmes Solana. Je vous recommande de les lire ensemble.

Qu’est-ce qu’un test ?

Les tests servent à vérifier qu’un segment de code ou une application entière fonctionne comme prévu. Il existe deux grandes catégories de tests :

  • Tests manuels : processus centré sur l’humain dans lequel les cas de test sont exécutés par des développeurs, des analystes en assurance qualité, des testeurs d’intrusion ou toute autre personne responsable
  • Tests automatisés : processus centré sur le code dans lequel des scripts sont écrits afin d’exécuter par programmation des cas de test prédéfinis

Les tests manuels constituent un processus très flexible, indépendant du type d’application testé. Ils conviennent bien aux nouvelles fonctionnalités, à l’utilisabilité et à l’accessibilité. Ils reposent sur l’intuition du testeur quant au comportement attendu d’une application. Cependant, ils sont intrinsèquement lents, sujets aux erreurs, chronophages et souvent incomplets, car tous les scénarios ne sont pas couverts, notamment en raison du manque d’outils dans le processus de test. 

Les tests automatisés cherchent à pallier les inconvénients des tests manuels. Ils sont généralement plus rapides, surtout lorsque les tests sont exécutés en parallèle. Ils sont moins sujets aux erreurs humaines puisqu’ils suivent un script prédéfini. Leur capacité à traiter efficacement un grand nombre de cas augmente la couverture des tests et en fait une solution hautement évolutive. Cependant, en raison de leur objectivité rigide, les tests automatisés sont moins précis pour les scénarios qui dépendent de l’interaction humaine, du jugement ou du raisonnement critique.

Dans cet article, nous nous concentrons sur les tests automatisés de nos programmes Solana, car les tester manuellement sur mainnet serait assez coûteux et les tester manuellement sur devnet prendrait beaucoup de temps. Les tests manuels comme automatisés doivent néanmoins faire partie du processus de test avant le déploiement du code en production. Un processus robuste permet de réduire le nombre de bugs introduits en production en les identifiant plus tôt pendant le développement.

Il existe différents types de tests automatisés, notamment :

  • Tests unitaires
  • Tests d’intégration
  • Tests de bout en bout (E2E)

Tests unitaires

Les tests unitaires consistent à tester les plus petites unités fonctionnelles du code afin de vérifier qu’elles fonctionnent correctement. Idéalement, ces unités sont les plus petits composants possibles d’un programme, par exemple des fonctions ou des modules individuels, qui forment le produit final une fois assemblés. L’idée centrale est que le programme dans son ensemble devrait fonctionner comme prévu si ses composants sont testés de manière approfondie. 

Les tests unitaires sont essentiels au développement sur Solana, car ils garantissent que chaque partie d’un programme se comporte comme prévu. Leur réutilisabilité permet de vérifier que les nouvelles fonctionnalités ou mises à jour respectent les spécifications du projet et les attentes des utilisateurs définies dans le cas de test. Les tests unitaires favorisent donc naturellement l’optimisation et la refactorisation du code afin que les améliorations n’affectent pas négativement les fonctionnalités du programme. Ils garantissent non seulement que chaque segment de code fonctionne correctement dans différentes conditions de test, mais aussi que les interactions avec la blockchain sont efficaces et sécurisées. La détection précoce des bugs grâce aux tests unitaires est cruciale, car elle empêche de potentielles vulnérabilités d’atteindre la production. 

Différents frameworks simplifient les tests unitaires en facilitant la simulation des conditions réseau et la gestion de l’état du programme, comme nous le verrons plus loin dans cet article. Les développeurs Solana peuvent atteindre un niveau élevé de fiabilité et de performance du code grâce aux tests unitaires.

Tests d’intégration

Les tests d’intégration vont au-delà des tests unitaires pour examiner la manière dont les différentes unités d’un programme fonctionnent ensemble. Vérifier que les fonctions et les modules d’un programme coopèrent correctement est crucial dans le développement sur Solana, où les interactions entre programmes sont souvent complexes et peuvent avoir des conséquences financières. Les tests d’intégration visent à identifier et à résoudre les problèmes qui ne sont pas immédiatement visibles lorsque les unités sont testées séparément, mais qui apparaissent lorsque les composants interagissent. Il peut s’agir d’incompatibilités de formats de données, d’incohérences de types, de dépendances entre programmes ou de problèmes liés à des API tierces.

Dans le contexte de Solana, où les programmes interagissent intrinsèquement avec d’autres programmes, des wallets et des oracles, les tests d’intégration vérifient que ces interactions se déroulent comme prévu. Même si chaque unité fonctionne parfaitement, leur combinaison peut introduire des comportements inattendus ou des inefficacités qui apparaissent dans ces conditions simulées. Les développeurs peuvent utiliser différents frameworks de test pour simuler divers flux de transactions et interactions entre programmes, en reproduisant fidèlement des scénarios réels. Par exemple, Bankrun est un framework de test robuste et léger qui permet aux développeurs d’avancer et de reculer dans le temps, ainsi que de définir dynamiquement les données des comptes. Ces opérations sont impossibles avec solana-test-validator. Les tests d’intégration sont indispensables pour garantir qu’un programme est robuste, fiable et prêt à répondre aux exigences des conditions réseau de Solana.

Tests de bout en bout (E2E)

Les tests de bout en bout (E2E) constituent l’aboutissement du processus de test. Ils visent à évaluer l’intégralité du flux opérationnel d’un programme tel qu’il se déroulerait dans des scénarios réels. Cette méthodologie diffère des tests unitaires et d’intégration, car elle examine le programme du point de vue de l’utilisateur : tous les flux et toutes les fonctionnalités qu’un utilisateur final pourrait rencontrer doivent fonctionner comme prévu. 

Les tests E2E sont essentiels pour vérifier qu’un programme répond à ses exigences fonctionnelles et offre une expérience utilisateur fluide. Cette phase permet de découvrir des problèmes qui n’étaient pas nécessairement visibles pendant les tests unitaires ou d’intégration, comme des retards de traitement des transactions, des difficultés de persistance de l’état, des optimisations des unités de calcul ou des conditions réseau inattendues. Bien que les tests E2E s’appliquent généralement à une dApp entière, tester les flux opérationnels d’un programme et vérifier comment la transaction d’un utilisateur interagit avec les différentes fonctions et les différents modules est crucial pour créer un programme performant et sécurisé.

Combiner ces méthodologies de test

Il est essentiel d’adopter une stratégie de test en plusieurs couches et de combiner les tests unitaires, d’intégration et E2E dans votre processus de développement. Chaque méthodologie joue un rôle distinct dans le cycle de développement et couvre différents aspects des fonctionnalités et des performances d’un programme.

Les tests unitaires constituent la base d’une approche en plusieurs couches. Ils permettent aux développeurs d’identifier et de résoudre rapidement les problèmes au niveau le plus granulaire du code. S’ils excellent à vérifier la justesse objective des fonctions ou modules individuels, ils ne tiennent pas compte de la façon dont ces unités fonctionnent ensemble ni de leur place dans l’expérience utilisateur.

Les tests d’intégration comblent cette lacune en évaluant la manière dont les différentes unités interagissent afin de détecter les problèmes qui apparaissent lors de leur intégration. Toutefois, ils ne suffisent pas toujours à reproduire pleinement l’expérience de l’utilisateur final ou le comportement du programme dans des conditions réelles.

Les tests E2E complètent les tests unitaires et d’intégration en simulant des scénarios utilisateur réels et en testant les applications dans leur ensemble. Cette approche est précieuse pour évaluer l’expérience utilisateur globale, mais elle ne fournit pas les informations granulaires nécessaires pour identifier et résoudre rapidement des problèmes précis.

En combinant ces méthodologies, les développeurs peuvent créer un framework de test robuste qui couvre tout l’éventail des problèmes potentiels. Une approche complète améliore non seulement la qualité et la sécurité du programme, mais rationalise également le processus de développement. Les développeurs peuvent prendre rapidement des décisions et effectuer des révisions en sachant que leurs modifications seront vérifiées à plusieurs niveaux. Il est indispensable de combiner ces méthodologies pour garantir, avant le déploiement, qu’un programme Solana est techniquement fiable et conforme aux attentes des utilisateurs dans des conditions réelles.

Écrire de bons tests

Écrire des tests efficaces est indispensable pour développer des programmes Solana fiables et sécurisés. Un bon test se concentre avant tout sur le comportement testé plutôt que sur le framework utilisé ou sur les détails d’implémentation du code. Les développeurs peuvent élaborer une stratégie efficace qui améliore la qualité du code en combinant les principes du développement piloté par les tests (TDD), le modèle Arrange-Act-Assert (AAA) et les bonnes pratiques du secteur.

Développement piloté par les tests (TDD)

Le TDD est une approche robuste du développement logiciel guidée par l’écriture de tests. Autrement dit, l’idée principale consiste à écrire les tests avant le code proprement dit. Le cycle TDD comporte généralement trois étapes :

  • Écrire un test qui échoue : le développement doit commencer par l’écriture d’un test portant sur la prochaine fonctionnalité que le développeur souhaite ajouter. Ce test échouera inévitablement, puisque la fonctionnalité testée n’existe pas encore
  • Implémenter le code : les développeurs doivent écrire le minimum de code nécessaire pour que le test réussisse. L’objectif est ici de privilégier la rapidité et la simplicité
  • Refactoriser : une fois le test réussi, les développeurs doivent refactoriser leur code afin d’en améliorer la structure et la clarté sans modifier son comportement. Le test réussi sert de filet de sécurité contre l’introduction de changements incompatibles. Cette étape peut inclure la suppression du code dupliqué, la division des méthodes en unités plus petites, la réorganisation des hiérarchies d’héritage ou l’adoption de noms explicites. Dans le contexte de Solana, elle peut consister à optimiser le nombre de CU demandées pour une transaction, à réduire le nombre de CPI ou à simplifier les flux de transactions.

Même si le TDD n’est pas indispensable pour créer de bons programmes Solana, les développeurs devraient tenir compte de sa philosophie : il favorise une approche méticuleuse de la création de programmes, son caractère itératif encourage un processus de développement flexible et adaptatif, et il répond parfaitement aux besoins de précision, de sécurité et d’efficacité. Le TDD encourage les développeurs à écrire un code plus propre, mieux ciblé et optimisé pour les exigences propres au réseau et aux performances de Solana, par exemple l’optimisation des CU. 

Le modèle Arrange-Act-Assert (AAA)

Le modèle AAA offre une structure simple mais puissante pour écrire des tests clairs, concis et efficaces. Il encourage une approche rigoureuse de l’écriture des tests, divisée en trois phases distinctes :

  • Arrange : commencez par configurer l’environnement de test et préparer toutes les entrées pertinentes. Cela peut impliquer de générer des comptes, de simuler leurs soldes ou de préparer des instructions. L’objectif est de créer un scénario contrôlé qui reproduit les conditions dans lesquelles le comportement sera testé
  • Act : exécutez le comportement testé. L’accent est mis sur l’action qui déclenche le comportement à tester. Par exemple, que se passe-t-il lorsque j’appelle la fonction x en lui transmettant le compte y ?
  • Assert : comparez le résultat de l’action au résultat attendu. Cette étape est essentielle pour déterminer si le test réussit ou échoue. Les assertions peuvent aller d’une simple vérification de valeur à des validations complexes impliquant plusieurs changements d’état. Leur implémentation dépend en définitive du framework ou du protocole utilisé. Lighthouse est un programme qui fournit des instructions d’assertion pouvant être ajoutées aux transactions pour identifier, par exemple, des états indésirables, des résultats de simulation falsifiés ou des dépenses excessives. Nous étudierons plus en détail les avantages et les subtilités de Lighthouse dans un autre article.

La force du modèle AAA réside dans son adaptabilité, qui le rend utile pour les tests unitaires, d’intégration et E2E. Par exemple :

  • Tests unitaires : le test d’une fonction peut préparer l’état du programme, exécuter la fonction, puis vérifier sa valeur de retour ou les changements d’état qui en résultent
  • Tests d’intégration : tester les interactions entre plusieurs programmes peut consister à préparer leur déploiement et leurs états initiaux, à exécuter les transactions pertinentes, puis à vérifier l’état final de chaque programme concerné
  • Tests E2E : le test E2E d’un programme peut préparer l’état du programme, exécuter l’intégralité d’un flux utilisateur attendu, par exemple créer un compte, créer une proposition, voter pour cette proposition et clôturer sa phase de vote, puis comparer les résultats du flux aux résultats attendus

Le modèle AAA est essentiel au développement de programmes. Il impose une approche des tests centrée sur le comportement, nécessaire pour vérifier qu’un programme agit comme prévu. Les tests structurés selon le modèle AAA sont plus faciles à comprendre et à maintenir, car chaque phase est clairement divisée entre préparation, action et vérification. De plus, AAA favorise la création de tests indépendants et découplés, centrés sur des comportements ou des interactions spécifiques.

Bonnes pratiques du secteur

L’écriture de bons tests n’est pas propre au développement sur Solana. Nous pouvons appliquer les enseignements généraux du développement logiciel aux tests des programmes Solana, en nous concentrant sur les comportements attendus plutôt qu’en nous laissant accaparer par les détails d’implémentation. 

Par exemple, les tests unitaires doivent généralement cibler l’interface publique d’une méthode, fournir des arguments précis et vérifier que les résultats correspondent aux attentes. Cette approche garantit que les tests unitaires restent valides même si l’implémentation interne de la méthode change, tant que son comportement demeure cohérent. Dans le contexte du développement sur Solana, cela signifie que les modifications apportées à la logique d’un programme qui n’affectent pas son comportement externe ne doivent nécessiter aucune refactorisation des tests.

Un piège courant lors de l’écriture de tests unitaires consiste à les rendre trop dépendants du fonctionnement interne de la méthode testée. Il peut s’agir d’exiger que certaines méthodes privées soient appelées un nombre précis de fois ou soient codées d’une manière particulière. Ces tests sont trop fragiles et risquent d’échouer après n’importe quelle refactorisation du code, même lorsque le comportement réel de la méthode testée demeure inchangé. Il faut plutôt se concentrer sur les résultats et les effets secondaires de la méthode observables de l’extérieur. Les outils de couverture du code permettent alors de vérifier que les tests sont complets sans dépendre excessivement des mécanismes internes.

L’adoption de ces bonnes pratiques dans le développement sur Solana améliore la robustesse et l’adaptabilité des programmes. En testant les comportements plutôt que les détails d’implémentation, vous obtenez un code plus résilient et plus facile à maintenir, ce qui est essentiel lors d’un déploiement dans un environnement réseau dynamique comme Solana. Cette approche garantit que les changements apportés à la logique du programme ne nécessitent pas de campagnes de tests approfondies et que le programme est prêt à être déployé.

Commençons maintenant à écrire quelques tests.

Écrire des tests unitaires simples

Tests unitaires en Rust

Rust adopte une approche particulière des tests unitaires en encourageant les développeurs à placer les tests dans les mêmes fichiers que leur code. Cela se fait au moyen du module tests et est conditionné par l’attribut #[cfg(test)]. L’attribut de test garantit que ces tests sont compilés et exécutés uniquement lorsque le logiciel est explicitement testé avec la commande cargo test, c’est-à-dire qu’ils ne sont pas exécutés avec la commande cargo build. Les développeurs peuvent également choisir d’exclure certains tests de l’exécution habituelle avec l’attribut #[ignore]. Cette option est utile pour les tests particulièrement lents, qui peuvent néanmoins être exécutés lorsqu’ils sont explicitement appelés avec la commande cargo test -- --ignored.

Prenons comme exemple la fonction Rust suivante :

Code
pub fn bubble_sort<T: Ord>(array: &mut [T]) {
    if array.is_empty() {
        return;
    }

    for i in 0..array.len() {
        for j in 0..array.len() - 1 - i {
            if array[j] > array[j + 1] {
                array.swap(j, j + 1);
            }
        }
    }
}

Le tri à bulles est un algorithme de tri qui parcourt plusieurs fois les éléments d’une liste, compare chaque élément à celui qui le suit et échange leurs valeurs si nécessaire. Pour vérifier que cette fonction se comporte comme prévu, nous pourrions écrire les tests suivants :

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

    #[test]
    fn test_bubble_sort() {

        let mut test1 = vec![12, 39, 4, 36, 777];
        assert_eq!(bubble_sort(&mut test1), vec![4, 12, 36, 39, 777]);

        let mut test2 = vec![21, 55, 14, -123, 32, 0];
        assert_eq!(bubble_sort(&mut test2), vec![-123, 0, 14, 21, 32, 55]);

        let mut test3 = vec!["Orange", "Pear", "Apple", "Grape", "Banana"];
        assert_eq!(bubble_sort(&mut test3), vec!["Apple", "Banana", "Grape", "Orange", "Pear"]);
    }
}

Cet exemple montre comment notre module tests est annoté avec l’attribut #[cfg(test)]. Dans ce module, nous importons tous les éléments publics du module parent dans la portée du module de test actuel avec use super::*;. Nous disposons ensuite de plusieurs cas de test dans lesquels nous vérifions la forme attendue des vecteurs triés. Rust fournit plusieurs macros d’assertion, comme assert! pour vérifier une condition générale, assert_eq! pour l’égalité et assert_ne! pour l’inégalité. Ces assertions sont au cœur de la stratégie de test de Rust : elles suffisent pour commencer à écrire des tests. 

Dans un exemple très simple, imaginez une fonction qui détermine si le solde d’un compte est suffisant pour payer une transaction donnée :

Code
pub fn has_sufficient_balance(account_balance: u64, transaction_fee: u64) -> bool {
    account_balance >= transaction_fee
}

Cette fonction accepte deux arguments : le solde actuel d’un compte donné et les frais de transaction attendus. Elle renvoie true si le solde du compte suffit à couvrir les frais de transaction, ou false dans le cas contraire. Elle pourrait être testée très simplement avec le test unitaire suivant :

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

    #[test]
    fn sufficient_funds_for_transaction() {
        let account_balance = 1_000_000;
        let transaction_fee = 5_000;

        assert!(has_sufficient_balance(account_balance, transaction_fee));
    }

    #[test]
    fn insufficient_funds_for_transaction() {
        let account_balance = 1_000;
        let transaction_fee = 5_000;

        assert!(!has_sufficient_balance(account_balance, transaction_fee));
    }
}

Dans le premier test, nous vérifions que has_sufficient_balance renvoie true lorsque le solde du compte est nettement supérieur aux frais de transaction, ce qui indique que les fonds sont suffisants pour couvrir la transaction. Dans le second, nous vérifions que has_sufficient_funds renvoie false lorsque le solde du compte est inférieur aux frais de transaction, ce qui indique que les fonds sont insuffisants. 

Autres points importants concernant les tests en Rust

Rust fournit l’attribut #[should_panic] pour signaler les tests qui sont censés déclencher une panique dans certaines conditions. Il est utile pour tester les chemins de gestion des erreurs et préciser les messages de panique attendus :

Code
#[test]
#[should_panic(expected = "Divide-by-zero error")]
fn test_divide_by_zero() {
    divide_non_zero_result(0, 0);
}

Contrairement à de nombreux autres langages, Rust permet de tester directement les fonctions privées. Les tests unitaires peuvent ainsi être plus détaillés et couvrir chaque aspect des fonctionnalités du code.

Rust prend également en charge des techniques d’organisation des tests plus avancées :

  • Modules imbriqués : dans les projets complexes, les tests peuvent être organisés en modules imbriqués afin de créer une structure hiérarchique claire qui reflète l’organisation du projet
  • Tests fondés sur des résultats : Rust permet aux tests de renvoyer un type Result<(), E>. Les développeurs peuvent ainsi utiliser l’opérateur ? dans les tests, ce qui rend la gestion des erreurs plus expressive

Tests unitaires en TypeScript avec Mocha et Chai

TypeScript s’est imposé comme un choix populaire pour tester les programmes, Anchor dominant largement en tant que lingua franca du développement Rust sur Solana. Avec la commande anchor init, le framework de test Mocha et la bibliothèque d’assertion Chai sont initialisés par défaut dans les nouveaux projets Anchor. 

Mocha est un framework de test JavaScript riche en fonctionnalités qui s’exécute sur Node.js. Il simplifie considérablement les tests asynchrones. Dans le développement sur Solana, Mocha sert principalement à tester la logique côté client des dApps et d’autres interactions avec la blockchain. 

Chai est une bibliothèque d’assertion compatible avec n’importe quel framework de test JavaScript, comme Mocha. Elle fournit aux développeurs diverses fonctions pour exprimer les assertions de manière lisible. Les interfaces expect, should et assert de Chai permettent d’écrire des tests complets, intuitifs à lire comme à rédiger. Avec les interfaces expect et should, elle utilise des chaînes de langage, c’est-à-dire des accesseurs chaînables, pour améliorer la lisibilité des assertions. Grâce à Chai, expect({a: 1, b: 2}).to.not.have.any.keys(“c”, “d”); constitue une assertion valide et très lisible.

Par exemple, si nous créons un projet hello_world avec la commande anchor init hello_world, le fichier de test hello_world.ts suivant est créé dans le répertoire hello_world/tests :

Code
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloWorld } from "../target/types/hello_world";

describe("hello_world", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
  });
});

Décomposons maintenant l’ensemble pour mieux le comprendre.

Mocha utilise des blocs describe pour regrouper les tests et des fonctions it pour définir les cas de test. Cet exemple respecte le modèle AAA afin de structurer la démarche de test :

  • Arrange : ici, anchor.setProvider(anchor.AnchorProvider.env()); configure le client Anchor pour qu’il utilise le provider par défaut de l’environnement, qui pointe généralement vers un validateur de test Solana local. Ensuite, la déclaration const program initialise une instance du programme à tester, ce qui nous permet d’appeler ses méthodes dans le test
  • Act : dans notre cas de test “Is initialized!”, nous appelons la méthode initialize de notre programme et envoyons la transaction
  • Assert : dans ce cas de test, nous journalisons la signature de la transaction sans fournir d’assertion. C’est généralement à cette étape que nous intégrons Chai pour les assertions. Dans un exemple très simple, nous pourrions modifier le code de test par défaut en ajoutant une assertion telle que expect(tx).to.be.a(“string”);. Un test plus détaillé pourrait récupérer et examiner l’état du programme après l’initialisation, puis vérifier qu’il correspond aux valeurs attendues

L’association de Mocha et de Chai avec le modèle AAA configuré par défaut dans les projets Anchor fournit un framework robuste pour tester les programmes. En préparant clairement l’environnement de test, en appelant les méthodes du programme, puis en vérifiant les résultats, les développeurs Solana peuvent garantir un fonctionnement prévisible et fiable des programmes.

Imaginons, par exemple, que vous développiez sur Solana un programme permettant aux utilisateurs de déposer et de retirer des SOL dans un coffre. Voici à quoi pourrait ressembler un test écrit en TypeScript avec Mocha et Chai pour vérifier que la fonctionnalité de dépôt fonctionne comme prévu :

Code
import { expect } from "chai";
import { PublicKey } from "@solana/web3.js";
import { depositSOL } from "../src/vault";

describe("Vault Program", function() {
    describe("Deposit functionality", function() {
        it("should correctly deposit SOL into the vault", async function() {
            const vaultPublicKey = new PublicKey(/* vault public key */);
            const userPublicKey = new PublicKey(/* user public key */);
            const depositAmount = 1; // 1 SOL

            const initialVaultBalance = await getVaultBalance(vaultPublicKey);
            await depositSOL(vaultPublicKey, userPublicKey, depositAmount);

            const finalVaultBalance = await getVaultBalance(vaultPublicKey);
            expect(finalVaultBalance).to.equal(initialVaultBalance + depositAmount);
        });
    });
});

Cet exemple teste une fonction hypothétique depositSOL, qui gère le dépôt de SOL dans un coffre. Il vérifie que le solde du coffre augmente du montant approprié après le dépôt. Nous utilisons la fonction getVaultBalance, supposée être une fonction utilitaire qui récupère le solde actuel du coffre.

Autres points importants concernant les tests en TypeScript avec Mocha et Chai

Le système de types statiques de TypeScript peut parfois compliquer l’écriture des tests, en particulier lorsqu’il faut gérer des types complexes ou mal définis. Utilisez des assertions de type pour éviter les problèmes liés aux types dans vos tests. Veillez toutefois à ce qu’elles ne masquent pas de potentielles erreurs d’exécution dues à des types incorrects.

Lorsque vous simulez des objets ou des fonctions dans TypeScript, assurez-vous que les entités simulées respectent les types appropriés. Des bibliothèques telles que ts-sinon ou ts-mockito peuvent aider à créer des objets simulés avec un typage sûr, afin que les tests restent précis et reflètent le comportement réel du programme.

Mocha fournit les méthodes only et skip pour exécuter exclusivement certains tests ou en ignorer. Si elles sont pratiques pendant le développement, il est facile de les valider accidentellement dans le code de production, ce qui entraîne des exécutions de tests incomplètes. Vérifiez toujours la présence de only ou de skip avant de transférer les tests en production. Soyez également prudent lorsque vous utilisez les hooks de Mocha, à savoir beforeEach, afterEach, before et after, avec du code asynchrone. Veillez à gérer correctement les promesses avec async/await, ou appelez la méthode de rappel done afin d’éviter les promesses non résolues ou les rappels non invoqués.

Lorsque vous utilisez expect().to.deep.equal() de Chai, tenez compte de son comportement avec les objets contenant des propriétés générées dynamiquement, comme des dates ou des valeurs aléatoires. Ces propriétés peuvent provoquer des échecs inattendus dans les tests qui attendent une égalité profonde. Lorsque cela s’applique, envisagez d’utiliser expect().to.include() de Chai pour effectuer des assertions plus ciblées.

Frameworks de test Solana populaires

Bankrun

Une banque assure le suivi des comptes clients, gère l’exécution des programmes et préserve l’intégrité ainsi que la progression du registre de Solana. Il s’agit essentiellement d’un instantané du registre à un instant donné, qui regroupe l’état résultant des transactions d’un bloc spécifique. 

Bankrun est un framework de test léger et flexible, écrit en Node.js pour les programmes Solana. Il met l’accent sur la simplicité d’utilisation et la rapidité, ce qui permet aux développeurs d’écrire et d’exécuter rapidement des tests sur leurs programmes. La véritable valeur de Bankrun tient au fait qu’il s’agit d’un framework de test qui permet aux développeurs de simuler des banques Solana et d’interagir avec elles dans un environnement contrôlé et efficace. Bankrun simplifie le processus de test en reproduisant la dynamique des banques Solana sans la surcharge habituellement associée à la configuration d’un tel environnement.

La conception de Bankrun repose sur un BanksServer léger qui reproduit le comportement d’un nœud RPC, mais avec des performances et une flexibilité nettement supérieures. Les développeurs peuvent interagir avec ce serveur via le BanksClient. Ce client fournit un ensemble complet d’outils avec des méthodes permettant de récupérer les soldes des comptes et les statuts des transactions, ainsi que de simuler des transactions. En particulier, la méthode tryProcessTransaction permet de traiter les transactions censées échouer sans générer d’erreur JavaScript. Les développeurs peuvent ainsi vérifier directement des modes d’échec ou des messages de journalisation spécifiques.

Le dépôt GitHub Futarchy de Meta-DAO est un excellent exemple d’utilisation de Bankrun pour tester du code prêt pour la production.

Intégration avec Anchor

L’intégration de Bankrun avec Anchor est très simple. Grâce à startAnchor, les développeurs peuvent déployer automatiquement dans l’environnement de test tous les programmes d’un espace de travail Anchor. Les tests reproduisent ainsi fidèlement le comportement du programme dans un environnement Solana complet. La documentation de Bankrun fournit l’exemple de code suivant :

Code
import { startAnchor } from "solana-bankrun";
import { PublicKey } from "@solana/web3.js";

test("anchor", async () => {
	const context = await startAnchor("tests/anchor-example", [], []);
	const programId = new PublicKey(
		"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
	);
	const executableAccount = await context.banksClient.getAccount(programId);
	expect(executableAccount).not.toBeNull();
	expect(executableAccount?.executable).toBe(true);
});

Le package anchor-bankrun est une extension puissante qui permet d’utiliser Anchor avec Bankrun en exportant une classe BankrunProvider pouvant remplacer directement AnchorProvider pendant les tests.

Écriture de comptes arbitraires avec Bankrun

L’une des fonctionnalités phares de Bankrun est sa capacité à écrire des données de compte arbitraires. Elle permet aux développeurs de contourner les limites liées aux états des comptes en offrant un degré de flexibilité sans précédent. Par exemple, un développeur peut simuler un compte détenant une quantité importante d’USDC sans posséder la paire de clés de création d’USDC. Cette possibilité est précieuse pour les tests, car elle évite de devoir manipuler de véritables tokens et simplifie ainsi la configuration de scénarios complexes. 

La documentation de Bankrun fournit un exemple de code permettant de créer une quantité illimitée d’USDC, qui illustre cette fonctionnalité au moyen de la fonction start. La fonction start prépare l’environnement de test en déployant les programmes et en définissant les données des comptes comme indiqué.

Voyage dans le temps

Une autre fonctionnalité autonome de Bankrun est sa capacité à voyager dans le temps, c’est-à-dire à manipuler la notion de temps à des fins de test. La manipulation du temps permet aux développeurs d’avancer ou de reculer instantanément l’horloge du cluster Solana, c’est-à-dire la sysvar Clock, afin de simuler des conditions temporelles précises. Cette possibilité est essentielle pour tester les programmes qui reposent sur une logique temporelle, notamment les calendriers d’acquisition, les verrouillages de tokens ou toute fonctionnalité déclenchée lorsqu’un instant précis est atteint.

Le voyage dans le temps est simple grâce à la méthode setClock. Elle permet aux développeurs de définir l’heure actuelle du cluster sur un horodatage Unix prédéfini, faisant ainsi basculer l’ensemble de l’environnement de test vers cet instant passé ou futur. Les opérations et les transactions du test se poursuivent comme si l’heure indiquée était l’heure actuelle, ce qui permet d’évaluer précisément le comportement du programme dans ces conditions.

Voici un exemple très simple montrant comment voyager dans le temps avec Bankrun :

Code
import { start } from "solana-bankrun";
import { PublicKey, Transaction, SystemProgram } from "@solana/web3.js";

async function simulateTimeTravel(context, secondsForward) {
    const newTimestamp = context.clock.unixTimestamp + secondsForward;
    context.adjustClock(newTimestamp);
}

test("One Year Later...", async () => {
    const context = await start([], []);
    const { banksClient, payer } = context;

    // Simulate setting the cluster clock forward by one year (in seconds)
    const oneYearInSeconds = 365 * 24 * 60 * 60;
    await simulateTimeTravel(context, oneYearInSeconds);

    // Proceed with tests assuming the future time
    const transaction = new Transaction().add(
        SystemProgram.transfer({
            fromPubkey: payer.publicKey,
            toPubkey: PublicKey.unique(),
            lamports: 100,
        }),
    );

    transaction.recentBlockhash = context.lastBlockhash;
    transaction.sign(payer);

    await banksClient.processTransaction(transaction);

    // Add assertions here to test expected future behavior
});

Bankrun ou solana-test-validator

Le choix entre Bankrun et solana-test-validator dépend largement des exigences propres aux scénarios de test. Grâce à sa rapidité, sa flexibilité et ses fonctionnalités spécialisées, Bankrun est le choix privilégié pour la plupart des scénarios de développement, en particulier ceux qui nécessitent des itérations rapides ou une simulation détaillée. Cependant, solana-test-validator reste pertinent pour les tests qui dépendent du comportement réel d’un validateur et qui utilisent des méthodes RPC non prises en charge par BanksServer.

solana-program-test

Le crate solana-program-test fournit un framework de test basé sur Rust et conçu spécialement pour les programmes Solana. Ce framework s’articule autour du BanksClient. Il simule les opérations d’une banque Solana, ce qui permet aux développeurs de déployer leurs programmes, d’interagir avec eux et d’évaluer leur comportement dans des conditions de test qui reproduisent le réseau principal, comme avec Bankrun. La structure ProgramTest complète le BanksClient en fournissant un utilitaire d’initialisation de l’environnement de test. Autrement dit, elle facilite le développement des programmes indiqués et la configuration des comptes nécessaires. D’autres structures comme BanksTransactionResultWithMetadata, InvokeContext et ProgramTestContext fournissent des informations détaillées et du contexte sur les transactions traitées pendant les tests, ce qui améliore l’ensemble du processus de débogage et de vérification. 

Pour simplifier le développement et les tests en local, solana-program-test précharge automatiquement plusieurs programmes :

  • SPL Token (et sa version 2022)
  • SPL Memo (versions 1.0 et 3.0)
  • SPL Associated Token Account

Ces programmes préchargés offrent une configuration de test plus rapide et plus ciblée, puisqu’il n’est pas nécessaire de configurer manuellement ces programmes courants.

Le dépôt GitHub de Marginfi contient plusieurs excellents exemples d’implémentation de solana-program-test dans du code prêt pour la production. Le guide de développement de Bonfida propose également une excellente présentation détaillée de l’écriture de tests d’intégration avec le framework solana-program-test.

solana-test-framework

solana-test-framework est une extension de solana-program-test développée par Halborn. Elle vise à enrichir l’environnement de test en ajoutant plusieurs méthodes pratiques à BanksClient, RpcClient, ProgramTest et ProgramTestContext. Comme avec Bankrun, les extensions de ProgramTestContext permettent par exemple de créer des scénarios de test avancés dans lesquels les développeurs peuvent accéder à des horodatages précis et mettre à jour les prix des oracles.

Ces extensions apportent les améliorations suivantes :

  • Gestion des transactions : elle simplifie l’assemblage, la signature et le paiement des transactions via transaction_from_instructions
  • Désérialisation des comptes : elle facilite respectivement la récupération et la désérialisation des comptes Anchor et Borsh avec get_account_with_anchor and get_account_with_borsh
  • Création de comptes et déploiement de programmes : elle permet aux développeurs de configurer efficacement leur environnement de test avec des fonctions comme create_account, create_token_mint, create_token_account et deploy_program.

solana-test-framework prend en charge les clusters externes comme l’environnement d’exécution simulé. Il est compatible avec plusieurs versions de Solana et d’Anchor, notamment les versions 1.9 à 1.14 de Solana et les versions correspondantes d’Anchor pour les versions 1.9, 1.10 et 1.14. 

Exemple de scénario de test

Le programme

Prenons le programme suivant comme exemple :

Code
use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;
use solana_program::program::invoke;

declare_id!("3vMZa7r3CpHGejvXYbUpPXmm54FxCDPF1QAYnnzL88J9");

#[program]
pub mod king_of_the_hill {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, initial_prize: u64) -> Result<()> {
        // In case the person who went first didn't send any SOL as the initial prize
        require!(initial_prize > 0, ErrorCode::NeedAnInitialPrize);

        let game_state = &mut ctx.accounts.game_state;

        game_state.king = ctx.accounts.initial_king.key();
        game_state.prize = initial_prize;

        let transfer_instruction = system_instruction::transfer(
            &ctx.accounts.initial_king.key(),
            &ctx.accounts.prize_pool.key(),
            initial_prize,
        );

        invoke(
            &transfer_instruction,
            &[
                ctx.accounts.initial_king.to_account_info(),
                ctx.accounts.prize_pool.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        Ok(())
    }

    pub fn become_king(ctx: Context<BecomeKing>, new_prize: u64) -> Result<()> {
        require!(
            new_prize > ctx.accounts.game_state.prize,
            ErrorCode::BidTooLow
        );

        let transfer_to_pool_instruction = system_instruction::transfer(
            &ctx.accounts.payer.key(),
            &ctx.accounts.prize_pool.key(),
            new_prize,
        );

        // Send the new king's funds to the pool
        invoke(
            &transfer_to_pool_instruction,
            &[
                ctx.accounts.payer.to_account_info(),
                ctx.accounts.prize_pool.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        // Send the old king's funds back
        ctx.accounts.prize_pool.sub_lamports(ctx.accounts.game_state.prize);
        ctx.accounts.king.add_lamports(ctx.accounts.game_state.prize);

        ctx.accounts.game_state.king = ctx.accounts.payer.key();
        ctx.accounts.game_state.prize = new_prize;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = initial_king,
        space = 8 + 32 + 8 + 1,
        seeds = [b"game_state"],
        bump,
    )]
    pub game_state: Account<'info, GameState>,
    #[account(mut)]
    pub initial_king: Signer<'info>,
    #[account(
        init,
        payer = initial_king,
        space = 8 + 8,
        seeds = [b"prize_pool"],
        bump,
    )]
    /// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
    pub prize_pool: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct BecomeKing<'info> {
    #[account(
        mut,
        has_one = king,
    )]
    pub game_state: Account<'info, GameState>,
    #[account(mut)]
    /// CHECK: This is okay - it's only receiving SOL and we don't need any other access
    pub king: UncheckedAccount<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        seeds = [b"prize_pool"],
        bump,
    )]
    /// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
    pub prize_pool: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct GameState {
    pub king: Pubkey,
    pub prize: u64,
    pub prize_pool_bump: u8,
}

#[error_code]
pub enum ErrorCode {
    #[msg("The initial prize must be greater than zero")]
    NeedAnInitialPrize,
    #[msg("The bid must be higher than the current prize")]
    BidTooLow,
    #[msg("Invalid prize pool account")]
    InvalidPrizePoolAccount,
}

Ce programme implémente un jeu simple de « roi de la colline » sur Solana. Le jeu permet aux utilisateurs de devenir le « roi » en envoyant plus de SOL à la cagnotte que le roi actuel. Les SOL envoyés par le roi précédent lui sont restitués lorsqu’un nouveau roi prend sa place.

Le programme fonctionne comme suit :

  • Initialiser : cette fonction configure le jeu avec un roi initial, c’est-à-dire le premier joueur à initialiser le jeu, et un montant initial pour la cagnotte. Ce montant doit être supérieur à zéro. La fonction transfère ensuite le montant initial du roi initial vers une cagnotte
  • Devenir roi : cette fonction permet à un nouveau joueur de devenir roi en misant plus de SOL que le montant actuel de la cagnotte. Elle transfère le montant actuel au roi sortant et met à jour la cagnotte avec la mise du nouveau roi, qui devient ainsi le nouveau roi. Cette mise doit être supérieure au montant actuel de la cagnotte

Écriture des tests

Le code suivant permet de tester avec succès le jeu du roi de la colline :

Code
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";

import { assert } from "chai";

const web3 = require("@solana/web3.js");

describe("King of the Hill Tests", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

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

  let initialKing, newKing;
  let gameStatePDA, prizePoolPDA;

  // Utility function for airdrops
  async function fundWallet(account, amount) {
    const publicKey = account.publicKey ? account.publicKey : account;

    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(publicKey, amount),
      "confirmed"
    );
  }

  before(async () => {
    initialKing = web3.Keypair.generate();
    newKing = web3.Keypair.generate();

    await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
    await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);

    [gameStatePDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("game_state")],
      program.programId
    );

    [prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("prize_pool")],
      program.programId
    );
  });

  it("Initializes the game correctly", async () => {
    // Arrange
    await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
    await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);

    let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);

    // Act
    const tx = await program.methods
      .initialize(initialPrize)
      .accounts({
        gameState: gameStatePDA,
        initialKing: initialKing.publicKey,
        prizePool: prizePoolPDA,
        systemProgram: web3.SystemProgram.programId,
      })
      .signers([initialKing])
      .rpc();

    // Assert
    let gameState: any = await program.account.gameState.fetch(gameStatePDA);
    assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
    assert.equal(
      gameState.prize.toString(),
      new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
    );
  });

  it("Changes the king correctly", async () => {
    // Arrange
    const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
    let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);

    // Act
    const becomeKingTx = await program.methods.becomeKing(newPrize)
      .accounts({
          gameState: gameStatePDA,
          king: initialKing.publicKey, // Correct usage of current king
          payer: newKing.publicKey, // New king who pays and becomes the king
          prizePool: prizePoolPDA,
          systemProgram: web3.SystemProgram.programId,
      })
      .signers([newKing]) // Signing by newKing
      .rpc();

    // Assert
    const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);

    const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
    assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");

    // Fetch the updated game state.
    const updatedGameState = await program.account.gameState.fetch(gameStatePDA);

    // Assertions to confirm the state has updated as expected.
    assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
    assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
  })
});

Examinons chaque élément en détail.

Commençons par les imports et la configuration de notre environnement de test dans Anchor. Pour cet exemple, j’effectue les tests en TypeScript avec Mocha et Chai sur localhost :

Code
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";

import { assert } from "chai";

const web3 = require("@solana/web3.js");

Nous utilisons describe pour regrouper nos cas de test. Nous configurons également le client pour qu’il utilise le cluster local, définissons correctement le programme, initialisons respectivement les variables du roi initial, du nouveau roi, du PDA de l’état du jeu et du PDA de la cagnotte, puis créons une fonction utilitaire qui simplifie les airdrops :

Code
describe("King of the Hill Tests", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

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

  let initialKing, newKing;
  let gameStatePDA, prizePoolPDA;

  // Utility function for airdrops
  async function fundWallet(account, amount) {
    const publicKey = account.publicKey ? account.publicKey : account;

    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(publicKey, amount),
      "confirmed"
    );
  }

// Other code

});

Nous utilisons ensuite le hook before pour configurer et approvisionner les paires de clés du roi initial et du nouveau roi, ainsi que pour dériver les PDA de l’état du jeu et de la cagnotte. Ce bloc ne s’exécute qu’une seule fois avant les cas de test, ce qui nous permet de simplifier plus clairement chaque cas selon le modèle AAA :

Code
before(async () => {
    initialKing = web3.Keypair.generate();
    newKing = web3.Keypair.generate();

    await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
    await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);

    [gameStatePDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("game_state")],
      program.programId
    );

    [prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("prize_pool")],
      program.programId
    );
});

Le premier cas de test est très simple : nous vérifions que notre jeu s’initialise correctement. Lors de la préparation, nous approvisionnons les PDA de l’état du jeu et de la cagnotte afin de pouvoir interagir avec eux par la suite, puis fixons le montant initial de la cagnotte à 1 SOL. Nous passons ensuite à l’action en appelant la méthode initialize avec le initialPrize. Pour les comptes, nous transmettons le PDA de l’état du jeu, le roi initial, le PDA de la cagnotte et le programme système. Le roi initial est le signataire de cette action. Enfin, nous vérifions que le roi et le montant de la cagnotte ont été correctement mis à jour dans l’état du jeu :

Code
it("Initializes the game correctly", async () => {
    // Arrange
    await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
    await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);

    let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);

    // Act
    const tx = await program.methods
      .initialize(initialPrize)
      .accounts({
        gameState: gameStatePDA,
        initialKing: initialKing.publicKey,
        prizePool: prizePoolPDA,
        systemProgram: web3.SystemProgram.programId,
      })
      .signers([initialKing])
      .rpc();

    // Assert
    let gameState: any = await program.account.gameState.fetch(gameStatePDA);
    assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
    assert.equal(
      gameState.prize.toString(),
      new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
    );
});

Le cas de test suivant vérifie qu’une autre personne peut devenir roi. Lors de la préparation, il récupère le solde initial du roi et fixe le nouveau montant de la cagnotte à 2 SOL. Il passe ensuite à l’action en appelant la fonction becomeKing et en lui transmettant le dernier montant de la cagnotte. Pour les comptes, nous transmettons le PDA de l’état du jeu, la clé publique du roi actuel, le nouveau roi en tant que payeur, le PDA de la cagnotte et le programme système. Le nouveau roi est défini comme signataire. La vérification consiste à confirmer que le roi récupère les SOL initialement versés pour devenir roi et que l’état du jeu a été correctement mis à jour :

Code
it("Changes the king correctly", async () => {
    // Arrange
    const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
    let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);

    // Act
    const becomeKingTx = await program.methods.becomeKing(newPrize)
      .accounts({
          gameState: gameStatePDA,
          king: initialKing.publicKey, // Correct usage of current king
          payer: newKing.publicKey, // New king who pays and becomes the king
          prizePool: prizePoolPDA,
          systemProgram: web3.SystemProgram.programId,
      })
      .signers([newKing]) // Signing by newKing
      .rpc();

    // Assert
    const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);

    const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
    assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");

    // Fetch the updated game state.
    const updatedGameState = await program.account.gameState.fetch(gameStatePDA);

    // Assertions to confirm the state has updated as expected.
    assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
    assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
})

Dans ce scénario de test, nous avons examiné les fonctionnalités principales de notre programme du roi de la colline. Grâce aux tests unitaires, nous avons validé en détail l’intégrité de la logique du programme. En vérifiant que le changement de roi s’effectue correctement, nous avons également confirmé que le programme fonctionne comme prévu dans des conditions réelles simulées. Ces tests soulignent l’importance d’une stratégie de test complète pour garantir la qualité et le bon fonctionnement de notre programme du roi de la colline. 

Conclusion

Les tests sont la pierre angulaire du développement de programmes Solana sécurisés, fiables et performants. Dans cet article, nous avons étudié l’importance de combiner les tests unitaires, d’intégration et E2E afin de couvrir toutes les étapes du cycle de développement d’un programme. En associant ces méthodologies et en exploitant de puissants frameworks de test comme Bankrun, solana-program-test et solana-test-framework, les développeurs peuvent améliorer considérablement la qualité de leurs programmes Solana. À mesure que vous progressez dans votre parcours de développeur Solana, appuyez-vous sur les principes, pratiques et exemples présentés ici pour créer des programmes robustes, performants et sécurisés.

Si vous avez lu jusqu’ici, merci, anon ! Saisissez votre adresse e-mail ci-dessous pour ne manquer aucune actualité de Solana. Vous souhaitez aller plus loin ? Découvrez les derniers articles sur le blog Helius et poursuivez votre parcours Solana dès aujourd’hui.

Ressources supplémentaires

Abonnez-vous à Helius

Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication

Image agrandie