Avant de commencer
Cet article ne montre pas les possibilités qu’un cadre de test de simulation peut faire ; il explique comment créer un cadre de test de simulation MVP pour votre projet. Pour des modifications plus avancées, faites vos propres recherches (DYOR) et consultez quelques liens inclus dans cet article.
REMARQUE : Les tests de simulation deviendront obsolètes à mesure que Sandbox apparaîtra. Consultez workspaces-rs et workspaces-js.
Courte introduction
La différence entre les tests unitaires et les tests de simulation est ; le premier est limité aux tests au sein de la bibliothèque ; tandis que ces derniers simulent un appel de l’extérieur vers l’intérieur. Les tests unitaires testent principalement les cadres internes pour une logique correcte ; tandis que les tests de simulation tentent de simuler les actions de l’utilisateur et de vérifier si les choses se passent comme prévu par le développeur. Traitez chaque fonction comme une simulation.
Si vous fantasmez sur un test, une affirmation, ne le faites pas. Même pour les tests unitaires, on n’est pas d’accord avec une assertion par cadre de fonction de test ; ce qui dit que les tests de simulation nécessitent plus d’une assertion dans une seule fonction/simulation.
Comme d’habitude, l’exemple le plus simple est l’application de salutation; nous allons donc l’utiliser et écrire un test de simulation pour cela.
Test de simulation de salutation
Vous n’avez pas besoin d’écrire votre propre application de salutation: exécutez simplement cette commande:
npx create-near-app --contract=rust <your_project_name>
Voila ! Vous avez maintenant un exemple d’application de salutation avec une interface simple. Suivez les instructions pour déployer le contrat et testez-le jusqu’à ce que vous soyez familier avant de continuer. Vérification des dernières lignes de la sortie.
We suggest that you begin by typing:
cd <your_project_name>
yarn dev
Copiez la commande et testez le fonctionnement de l’application jusqu’à ce que vous la connaissiez. Après le test, stoppez temporairement le déploiement : c’est assez embêtant de redéployer à chaque fois qu’on fait un ou des petits changement(s) de son point de vue si on n’a pas besoin du frontend.
Créer un dossier de test de simulation
Sans tenir compte de NEAR, les tests de simulation sont également appelés tests d’intégration dans (pur) Rust. Si vous souhaitez en savoir plus, veuillez consulter The Rust Book ou Rust By Example pour savoir comment effectuer des tests d’intégration.
En bref, vous avez besoin d’un dossier entièrement en dehors de src appelé tests. Nous allons donc le faire maintenant. En supposant que vous êtes déjà dans le répertoire de votre projet, exécutez ceci:
cd contract
mkdir tests
mkdir tests/sim
touch tests/sim/main.rs
Naviguez et ouvrez <your_project_name>/contract/tests/sim/main.rs et ouvrez-le dans l’éditeur de votre choix.
Maintenant, vous pouvez soit tout vider dans un seul fichier, soit le diviser. Généralement, vous avez un utils.rs pour faire des fonctions non test qui sont réutilisées. Vous pouvez avoir un autre fichier .rs pour plusieurs tests courts. Si vous avez des épreuves plus longues, comme les épreuves de simulation du contrat de huis clos, on propose un dossier par épreuve ; et nommez votre fichier en conséquence. Vous ne voudriez pas que vos lecteurs voient un fichier avec plus de 1000 lignes de code.
Maintenant, même si le Greeting n’aurait pas de longs tests de simulation, on va vous montrer ce que l’on entend par un test par fichier en les séparant volontairement. Vous n’êtes pas obligé de le faire, cependant.
Inclure les dépendances
Nous avons besoin d’une bibliothèque appelée near-sdk-sim. Cet exemple utilise le quasi-sdk v3.1.0, nous utiliserons donc le quasi-sdk-sim v3.2.0 correspondant (on ne sait pas pourquoi ils ne correspondent pas, cependant). Gardez à l’esprit, cependant, si la version ne correspond pas, les tests de simulation ne s’exécuteront pas, car il indique Import near_sdk :: some_fn n’est pas égal à near_sdk :: some_fn, ce qui est déroutant (mais en fait near_sdk :: some_fn est en effet différent de near_sdk::some_fn : ils ont des versions différentes !)
On aime en fait écrire avec le quasi-sdk v4.0.0-pre.4, mais cela nécessite quelques modifications du contrat pour qu’il fonctionne. Les changements ne sont pas compliqués, principalement à propos de AccountId n’est plus String et d’autres éléments plus petits ; mais ce n’est pas le but de cet article, nous allons donc nous en tenir à 3.1.0 et peut-être mettre à jour cet article à l’avenir s’il est mis à jour. (De plus, il y a un bug avec la simulation v4 dont nous parlerons plus tard).
Passons à Cargo.toml : il devrait maintenant ressembler à ceci : (voir nouvelle section dev-dependencies). Il est important de s’assurer que rlib est également présent !
[package]
name = "greeter"
version = "0.1.0"
authors = ["Near Inc <[email protected]>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
near-sdk = "3.1.0"
[dev-dependencies]
near-sdk-sim = "3.2.0"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []
Auparavant, l’un d’entre eux avait rédigé un guide sur la réduction de la taille des contrats ; y compris la suppression de “rlib”. Pour utiliser des tests de simulation, cependant, cela nécessite “rlib”, c’est donc un compromis entre la taille du contrat et la réalisation de tests de simulation.
Si vous avez arrêté le développement de fils, compilez maintenant le contrat une fois pour télécharger la bibliothèque. En contrat, lancez:
cargo build
Assurez-vous d’avoir beaucoup d’espace disque (espace libre recommandé de 20 Go) car cela prend beaucoup d’espace !
Pour une expérience fluide lors de la première compilation, essayez de compiler sur une machine dotée de 16 vCPU et de 8 Go de RAM. En particulier, librocksdb-sys prendra beaucoup de temps à compiler. Il n’est pas clair si la compilation est transférable (certainement pas à travers le système d’exploitation, mais incertain au sein du même système d’exploitation). Depuis que l’on a loué une VM sur Azure, on pouvait facilement changer la taille temporairement et la changer à une taille plus petite (et moins chère) après la compilation, donc pas de conflit.
Revenons en arrière pour écrire les tests de simulation dans main.rs.
Préparer le fichier wasm
Attention: chaque fois que vous apportez des modifications à votre contrat, vous devez reconstruire le wasm. Nous avons créé un script ici pour créer votre fichier wasm et le déplacer vers le dossier res du répertoire supérieur.
Alors exécutez ceci: (depuis le répertoire du contrat)
mkdir res
touch build.sh
Ensuite, incluez le contenu ci-dessous dans contract/build.sh afin de pouvoir exécuter bash build.sh (dans le dossier contract) au lieu de taper la commande à chaque fois.
#!/bin/bash
set -e
export WASM=greeter.wasm
RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/$WASM res/
Import
C’est facile : nous traitons la bibliothèque comme une bibliothèque. Rappelez-vous, ceux qui sont pub(crate) à l’origine dans la bibliothèque ne peuvent pas être utilisés dans les tests de simulation (puisque c’est à l’extérieur). C’est comme si vous le compiliez et que quelqu’un utilise votre code. Assurez-vous d’importer les fonctions dont vous avez besoin.
Si nous vérifions src.lib.rs, nous voyons qu’il y a (seulement) une structure Welcome. Lorsque vous l’importez, vous ajoutez le mot-clé Contract derrière. Par example:
use greeter::{ WelcomeContract };
Pour le contrat de blocage, leur structure principale s’appelle LockupContract, donc quand ils importent, c’est LockupContractContract. On ne sait pas pourquoi ils l’ont fait de cette façon, peut-être pour le non-conflit ; il suffit de l’ajouter !
Inclure les fichiers de contrat
La prochaine chose avant de tester que cela fonctionne, est d’inclure le fichier wasm. C’est un must. De plus, si vous avez un utils.rs, cela ne devrait PAS y être placé ; sinon, vous devez réfléchir sérieusement à la manière de le rendre détectable à partir d’autres fichiers. Pour ne pas trop réfléchir, on le met dans main.rs:
// Directories are relative to top-level Cargo directory. near_sdk_sim::lazy_static_include_bytes! { GREETER_WASM_BYTES => "res/greeter.wasm" // other wasm bytes. }
Ce que l’on entend par « annuaire de fret de premier niveau », on entend l’annuaire des contrats. Vous pouvez certainement découvrir des choses en dehors avec ../../some_file si vous en avez besoin. Par exemple, si vous n’utilisez pas le res mais le yarn dev, le out/main.wasm est en dehors du répertoire contract. Pour importer cela, nous faisons:
near_sdk_sim::lazy_static_include_bytes! { GREETER_WASM_BYTES => "../out/main.wasm" }
Puisqu’il s’agit d’une macro, assurez-vous de ne pas mettre accidentellement une “virgule” (“,””) après le dernier élément ; sinon, vous pourriez recevoir des messages d’erreur bizarres et Rust refuserait de compiler.
Malheureusement, nous n’avons pas pu vraiment tester cette fonction jusqu’à ce que nous ayons créé la fonction d’assistance (un MVP complet) et un test MVP.
Fonction d’initialisation
Pour ne pas répéter la fonction de configuration, nous les incluons dans basic_setup(). Vérifiez basic_setup() du contrat de verrouillage pour un autre exemple (qui inclut le déploiement d’autres contrats que leur contrat de test principal). Ici, nous ferons également la même chose, mais nous n’avons pas d’autre contrat à configurer, nous allons donc ignorer cela et inclure simplement les fonctions nécessaires dans basic_setup().
Créez un fichier utils.rs:
touch tests/sim/utils.rs
Dans utils.rs, nous insérons le contenu:
use crate::*; /// 300 TGas pub const MAX_GAS: u64 = 300_000_000_000_000; /// 1 NEAR (just a random number) pub const MIN_BALANCE_FOR_STORAGE: u128 = 1_000_000_000_000_000_000_000_000; pub const GREETER_ACCOUNT_ID: &str = "greeter"; pub(crate) fn basic_setup() -> (UserAccount, UserAccount) { let mut genesis_config = GenesisConfig::default(); genesis_config.block_prod_time = 0; let root = init_simulator(Some(genesis_config)); let alice = root.create_user( "alice".to_string(), to_yocto("200") ); (root, alice) }
Il y a des choses importantes à noter ici. Le premier est ce bloc de code:
let mut genesis_config = GenesisConfig::default(); genesis_config.block_prod_time = 0; let root = init_simulator(Some(genesis_config));
La Genesis est le premier bloc de la blockchain. Cependant, la genèse et le temps de genèse ne sont pas vraiment les mêmes. Par exemple, la racine représente la blockchain elle-même. Ce n’est pas le compte de niveau supérieur proche ou testnet : c’est la blockchain. Cependant, si vous consultez l’explorateur sur testnet, nous voyons qu’il a été créé pendant la Genèse. Ainsi, la racine vient en premier, puis elle est emballée avec certains des comptes à l’époque de Genesis. Nous créons un simulateur de la Genesis appelé root.
Ici, ce que nous entendons par Genèse n’est pas le temps de la genèse, mais la « racine ultime ». C’est le “compte parent” de tous les comptes de niveau supérieur.
Habituellement, nous n’avons pas besoin de modifier GenesisConfig ; et si vous avez besoin, c’est un exemple.
Si vous avez besoin d’apporter des modifications à la genèse, consultez la documentation pour les valeurs que vous pouvez modifier. Ensuite, vous pouvez le modifier ligne 2 dans le bloc de code ci-dessus en attribuant une valeur à chaque champ. Enfin, vous devez initialiser le simulateur avec init_simulator.
Si vous n’avez pas besoin de modification, vous pouvez initialiser un simulateur sans configuration (qui utilisera la valeur par défaut) comme ceci :
let root = init_simulator(None);
Ensuite, nous avons la racine qui crée un compte appelé “alice” pour nous. Le premier argument est le nom du compte, le second est le nombre de NEAR à donner au compte.
Parce que root est Genesis, il ne peut créer que des comptes de niveau supérieur comme near, testnet, alice. Il ne peut pas créer de sous-comptes comme alice.near : seul le compte parent near peut créer alice.near, pas Genesis.
Une chose que nous n’avons pas ici est le déploiement avec root. Pour notre contrat, nous utilisons le déploiement! macro que nous ferons dans la fonction de test au lieu d’ici. Mais si vous avez un autre fichier wasm, comme le fait le contrat de verrouillage, ils ne peuvent pas utiliser le déploiement ! macro, donc c’est comme ça qu’ils l’ont fait.
Par exemple sur le contrat whitelist ; il est déployé sur root comme ceci:
let _whitelist = root.deploy_and_init( &WHITELIST_WASM_BYTES, STAKING_POOL_WHITELIST_ACCOUNT_ID.to_string(), "new", &json!({ "foundation_account_id": foundation.valid_account_id(), }).to_string().into_bytes(), to_yocto("30"), MAX_GAS, );
Parce que l’exigence pour une fonction init d’être appelée une fois pendant le déploiement est si courante, il y a une fonction deploy_and_init. Si le contrat n’a pas de fonction de déploiement (en supposant que la liste blanche n’en ait pas ici), nous pouvons le faire.
let _whitelist = root.deploy( &WHITELIST_WASM_BYTES, STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), to_yocto("30") );
et en réalité il n’y a pas de fonction deploy_and_init, nous l’appelons donc manuellement. Pour ce faire, nous avons besoin d’un compte pour l’appeler qui ont la capacité de le faire. Pour le huis clos, c’est la fondation.
let foundation = root.create_user("foundation".to_string(), to_yocto("10000")); foundation.call( STAKING_POOL_WHITELIST_ACCOUNT_ID.to_string(), "new", &json!({ "foundation_account_id": foundation.valid_account_id() }).to_string().into_bytes(), MAX_GAS, NO_DEPOSIT, ).assert_success();
Nous notons que la réalité et la simulation présentent quelques différences.
Enfin, n’oubliez pas de l’importer dans main.rs et d’importer les fonctions requises:
use near_sdk::Balance; use near_sdk_sim::runtime::GenesisConfig; use near_sdk_sim::{init_simulator, to_yocto, UserAccount}; pub(crate) mod utils; use utils::*;
Construire la première fonction de test
Nous sommes prêts à créer la première fonction de test. Tout d’abord, importez les fonctions requises dans main.rs:
use near_sdk_sim::{deploy};
Il s’agit de la macro de déploiement.
Créez un fichier pour le test :
touch tests/sim/test_set_and_get_greeting.rs
Comme on vient d’un milieu Python, on aime nommer les fonctions en commençant par test. Vous n’êtes pas obligé. Ici, on adopte de nommer le nom du fichier commençant par test_; tandis que la fonction de test d’action à l’intérieur sans. Par exemple, nous aurons une fonction set_and_get_greeting() dans test_set_and_get_greeting.rs (fichier).
Importez le fichier dans main.rs avant d’oublier :
mod test_set_and_get_greeting;
Nous n’avons pas besoin de pub(crate) comme utils car il n’a pas besoin de partager quoi que ce soit avec d’autres fichiers.
La première chose dont nous avons besoin dans la fonction set_and_get_greeting est de déployer le contrat.
let greeter_amount = to_yocto("1000"); let (root, alice) = basic_setup(); let greeter = deploy!( contract: WelcomeContract, contract_id: GREETER_ACCOUNT_ID.to_string(), bytes: &GREETER_WASM_BYTES, signer_account: root, deposit: MIN_BALANCE_FOR_STORAGE + greeter_amount );
Si nous avons une méthode personnalisée #[init], nous incluons ces arguments après le dépôt:
gas: MAX_GAS, init_method: <method_name>(method_args)
Cependant, si nous n’en avons pas, nous les supprimons. Pour un tas de traits qui correspondent à la macro, consultez la documentation. Vous devez correspondre à au moins l’un d’entre eux ; sinon Rust refuse de compiler.
(Question : l’ordre est-il important ? Ou simplement le groupe de kwargs doit correspondre à l’un des traits ?)
Notez que contrairement à la réalité, le déploiement se fait à nouveau par root (vous pouvez le voir à partir de signer_account). En réalité, c’est fait par un compte qui en est responsable.
Ensuite, définissons une salutation et obtenons la salutation et affirmons qu’ils sont comme prévu.
Il semble que les gens aiment attribuer à une variable un appel res qui est réutilisé maintes et maintes fois. Ce n’est pas le moyen le plus clair; mais nous pouvons sûrement le faire pour ne pas nous casser la tête et penser à des noms de variables. res signifie simplement “résultats” renvoyés par un appel de fonction particulier.
C’est une bonne pratique d’attribuer à votre résolution un type (indépendamment du fait que Rust puisse déduire le type ou non), afin que vous sachiez quel type est renvoyé.
N’oubliez pas que nous avons view_method et change_method dans le contrat intelligent. Pour le contrat déployé avec deploy! (qui est le contrat intelligent que vous pouvez importer et celui que vous testez principalement), nous pouvons utiliser respectivement view_method_call et function_call. Nous parlerons dans un moment si nous avons un savoir externe sur la façon d’appeler.
Notre set_greeting est une change_method, nous allons donc utiliser un function_call. Un function_call prend un PendingContractTx, Gas et Deposit.
Le PendingContractTx n’est que la fonction, et les autres arguments sont faciles à interpréter. Voyons notre set_greeting :
let greeting: String = "Hello, NEAR!".to_owned(); alice.function_call( greeter.contract.set_greeting(greeting), MAX_GAS, 0 ).assert_success();
Assurez-vous de transmettre les arguments respectifs dans la fonction. Nous appelons également assert_success() à la fin pour nous assurer que la promesse est tenue. Ce qui précède imite le quasi-cli:
near call $GREETER_CONTRACT set_greeting '{
"message": "Hello, NEAR!"
}' --gas=$MAX_GAS --amount=0 --accountId=$ALICE_ACCOUNT_ID
Ensuite, nous pouvons avoir l’appel de la fonction de vue. Si vous cochez la fonction, get_greeting prend un identifiant de compte de type chaîne et renvoie une chaîne.
let res: String = alice.view_method_call( greeter.contract.get_greeting(alice.account_id().to_string()) ).unwrap_json();
On soupçonne que vous n’avez pas besoin de .account_id().to_string(), juste .account_id() est suffisant. Ici, nous le rendons juste explicite parce qu’il prend une chaîne. S’il prend AccountId, nous pourrions simplement appeler .account_id() sans aucune confusion. (Surtout lorsque AccountId n’est plus égal à String commençant près de sdk-rs v4.)
Comme le résultat renvoyé est un JSON, nous le déballons avec unwrap_json().
Ensuite, nous pourrions faire des affirmations sur le résultat.
assert_eq!(res, greeting);
Le message d’accueil de rappel est une variable que nous avons assignée plus tôt, qui est “Hello, NEAR!”.
Exécution du test d’intégration
Si vous souhaitez simplement exécuter le test d’intégration, exécutez cargo test –tests sim (car il se trouve dans le dossier tests/sim). Si vous souhaitez exécuter tous les tests, y compris les tests unitaires, exécutez le test de chargement.
Notez que pour une raison quelconque, cela prend environ 30 secondes ou plus (quel que soit le nombre de cœurs de processeur dont vous disposez); vous devez attendre avant même que le test ne commence.
Encore un rappel : si vous apportez des modifications au contrat, vous devez le reconstruire ; sinon, vous vous demanderez pourquoi il ne fonctionne pas et vous pensez qu’il fonctionnera maintenant, alors…
Complete Code
Vous pouvez trouver le code complet ici : https://github.com/Wabinab/NEAR_greeter_integration_testshttps://github.com/Wabinab/NEAR_greeter_integration_tests
Conclusion
Maintenant, vous pouvez répéter d’autres tests (si vous en avez) en créant un nouveau fichier, le lier en utilisant mod à main.rs, écrire les tests à l’intérieur. C’est un exercice amusant : plus vous écrivez, plus vous comprenez.
Remarque sur la mise à niveau vers v4.0.0-pre.4
La fonction deploy_and_init du contrat de liste blanche doit être modifiée:
AccountId n’est plus une chaîne
- Remplacez donc tous les “alice”.to_string() par “alice”.parse().unwrap(). Si le remplacement se trouve dans une fonction qu’il ne peut pas analyser, vous devez créer une variable. (Cela est particulièrement vrai dans la macro deploy!, qui n’a pas d’inférence de type).
let alice_account: AccountId = "alice".parse().unwrap(); // pass it to the function.
- valid_account_id est obsolète. Utilisez plutôt account_id(). Cela se produit dans json!.
- Tout entier passé à json ! nécessite une spécification. Exemple : la v3.2.0 autorise 10, mais la v4.0.0-pre.4 ne l’autorise pas : vous devez dire 10u64 ou tout autre type.
- La macro #[quickcheck] a un bogue et échoue au test avec la v4. Un fichier d’erreur sur Github ; au moment de la rédaction, l’équipe de développement ne répond pas encore.