Waffle: Bouchonnage dynamique et tests de contrats
À quoi sert ce tutoriel ?
Dans ce tutoriel, vous apprendrez comment :
- utiliser le bouchonnage dynamique
- tester les interactions entre contrats intelligents
Prérequis :
- vous savez déjà écrire un simple contrat intelligent en
Solidity
- vous vous débrouillez en
JavaScript
et enTypeScript
- vous avez fait d'autres tutoriels
Waffle
ou vous connaissez deux ou trois choses à ce sujet
Bouchonnage dynamique
Pourquoi le bouchonnage dynamique est-il utile ? Eh bien, il nous permet de rédiger des tests unitaires plutôt que des tests d'intégration. Qu'est-ce que cela signifie ? Cela signifie que nous n'avons pas à nous soucier des dépendances des contrats intelligents, donc que nous pouvons tous les tester de façon isolée. Laissez-moi vous montrer comment procéder.
1. Projet
Avant de commencer, nous avons besoin de préparer un simple projet node.js :
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# or if you're using npmnpm init
Commençons par ajouter typescript et les dépendances de test - mocha & chai :
yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# or if you're using npmnpm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev
Maintenant, ajoutons Waffle
et ethers
:
yarn add --dev ethereum-waffle ethers# or if you're using npmnpm install ethereum-waffle ethers --save-dev
La structure de votre projet devrait ressembler à ceci :
1.2├── contracts3├── package.json4└── test
2. Contrat intelligent
Pour démarrer un bouchonnage dynamique, nous avons besoin d'un contrat intelligent avec des dépendances. Ne t'inquiètes pas, nous assurons tes arrières !
Voici un contrat intelligent simple écrit en Solidity
dont le seul but est de vérifier si nous sommes riches. Il utilise un jeton ERC20 pour vérifier si nous avons suffisamment de jetons. Mettez-le dans ./contracts/AmIRichAlready.sol
.
1pragma solidity ^0.6.2;23interface IERC20 {4 function balanceOf(address account) external view returns (uint256);5}67contract AmIRichAlready {8 IERC20 private tokenContract;9 uint public richness = 1000000 * 10 ** 18;1011 constructor (IERC20 _tokenContract) public {12 tokenContract = _tokenContract;13 }1415 function check() public view returns (bool) {16 uint balance = tokenContract.balanceOf(msg.sender);17 return balance > richness;18 }19}Afficher toutCopier
Comme nous voulons utiliser le bouchonnage dynamique, nous n'avons pas besoin de tout l'ERC20, c'est pourquoi nous utilisons l'interface IERC20 avec une seule fonction.
Il est temps de créer ce contrat ! Pour cela, nous utiliserons le Waffle
. Tout d'abord, nous allons créer un simple fichier de configuration waffle.json
qui spécifie les options de compilation.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}Copier
Nous sommes désormais prêts à construire le contrat avec Waffle :
npx waffle
Facile, n'est-ce pas? Dans le dossier build/
, deux fichiers correspondant au contrat et à l'interface apparaissent. Nous les utiliserons plus tard pour les tests.
3. Tests
Nous allons créer un fichier appelé AmIRichAlready.test.ts
pour le test actuel. Tout d'abord, nous devons gérer les importations. Nous en aurons besoin pour plus tard:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"
Sauf pour les dépendances JS, nous devons importer le contrat et l'interface précédemment créés :
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"
Waffle utilise chai
pour le test. Cependant, avant de pouvoir l'utiliser, nous devons injecter les matchers de Waffle dans le chai lui-même :
1use(solidity)
Nous devons implémenter la fonction beforeEach()
qui réinitialisera l'état du contrat avant chaque test. Réfléchissons d'abord à ce dont nous avons besoin. Pour déployer un contrat, nous avons besoin de deux choses: un wallet et un contrat ERC20 déployé pour le passer comme argument pour le contrat AmIRichAlready
.
Premièrement, créons nous un portefeuille:
1const [wallet] = new MockProvider().getWallets()
Ensuite, nous devons déployer un contrat ERC20. Voici la partie délicate - nous n'avons qu'une seule interface. C'est la partie où Waffle vient nous sauver. Waffle a une fonction magique deployMockContract()
qui crée un contrat en utilisant uniquement le abi de l'interface :
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)
Maintenant, avec le wallet et l'ERC20 déployé, nous pouvons continuer et déployer le contrat AmIRichAlready
:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])
Avec tout cela, notre fonction beforeEach()
est terminée. Pour l'instant, votre fichier AmIRichAlready.test.ts
devrait ressembler à ceci :
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"910import IERC20 from "../build/IERC20.json"11import AmIRichAlready from "../build/AmIRichAlready.json"1213use(solidity)1415describe("Am I Rich Already", () => {16 let mockERC20: Contract17 let contract: Contract18 let wallet: Wallet1920 beforeEach(async () => {21 ;[wallet] = new MockProvider().getWallets()22 mockERC20 = await deployMockContract(wallet, IERC20.abi)23 contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address])24 })25})Afficher tout
Écrivons le premier test pour le contrat AmIRichalready
. De quoi pensez-vous que notre test devrait traiter? Ouais, vous avez raison! Nous devrions vérifier si nous sommes déjà riches :)
Mais attendez une seconde. Comment notre contrat fictif saura-t-il quelles valeurs retourner? Nous n'avons implémenté aucune logique pour la fonction balanceOf()
. Là encore, Waffle peut nous aider. Notre contrat fictif a de nouveaux trucs fantaisistes maintenant :
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)
Avec cette connaissance, nous pouvons enfin écrire notre premier test :
1it("returns false if the wallet has less than 1000000 tokens", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 expect(await contract.check()).to.be.equal(false)4})
Décomposons ce test en parties :
- Nous avons fixé notre contrat fictif ERC20 pour toujours retourner le solde de 999999 jetons.
- Vérifiez si la méthode
contract.check()
retournefalse
.
Nous sommes prêts à allumer la bête :
Alors le test fonctionne, mais... il y a encore des choses à améliorer. La fonction balanceOf()
retournera toujours 99999. Nous pouvons l'améliorer en spécifiant un portefeuille pour lequel la fonction devrait retourner quelque chose - tout comme un contrat réel :
1it("returns false if the wallet has less than 1000001 tokens", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 expect(await contract.check()).to.be.equal(false)6})
Jusqu'à présent, nous avons testé seulement le cas où nous ne sommes pas assez riche. Essayons l'inverse:
1it("returns true if the wallet has at least 1000001 tokens", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("1000001"))5 expect(await contract.check()).to.be.equal(true)6})
Vous exécutez les tests...
Et voici où tu en es! Notre contrat semble fonctionner comme prévu :)
Test des appels de contrat
Résumons ce qui a été fait jusqu'à présent. Nous avons testé la fonctionnalité de notre contrat AmIRichalready
et il semble qu'il fonctionne correctement. Cela signifie que nous avons terminé, n'est-ce pas? Pas exactement ! Waffle nous permet de tester encore plus notre contrat. Mais comment exactement ? Eh bien, dans l'arsenal de Waffle, il y a une correspondance entre calledOnContract()
et calledOnContractWith()
. Cela va nous permettre de vérifier si notre contrat a appelé le contrat fictif ERC20. Voici un test de base avec l'une de ces correspondance:
1it("checks if contract called balanceOf on the ERC20 token", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 await contract.check()4 expect("balanceOf").to.be.calledOnContract(mockERC20)5})
Nous pouvons aller encore plus loin et améliorer ce test avec l'autre matcher dont nous vous avons parlé:
1it("checks if contract called balanceOf with certain wallet on the ERC20 token", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 await contract.check()6 expect("balanceOf").to.be.calledOnContractWith(mockERC20, [wallet.address])7})
Vérifions si les tests sont corrects :
Super, tous les tests sont verts.
Tester des appels de contrats avec Waffle est super facile. Et voici la meilleure partie. Ces matchers fonctionnent à la fois avec des contrats normaux et fictifs ! C'est parce que Waffle enregistre et filtre les appels EVM plutôt que d'injecter du code, comme c'est le cas des bibliothèques de test populaires pour d'autres technologies.
La fin
Félicitations ! Maintenant vous savez comment utiliser Waffle pour tester dynamiquement les appels de contrats et les contrats fictifs. Il y a beaucoup plus de fonctionnalités intéressantes à découvrir. Je recommande de plonger dans la documentation de Waffle.
La documentation Waffle est disponible here(opens in a new tab).
Le code source de ce tutoriel est disponible ici(opens in a new tab).
Voici d'autres tutoriels qui pourraient vous intéresser :
Dernière modification: @nhsz(opens in a new tab), 27 février 2024