Passer au contenu principal

Waffle: Bouchonnage dynamique et tests de contrats

wafflecontrats intelligentssoliditytestbouchonnage
Intermédiaire
Daniel Izdebski
14 novembre 2020
7 minutes de lecture minute read

À 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 en TypeScript
  • 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-mocking
cd dynamic-mocking
mkdir contracts src
yarn init
# or if you're using npm
npm 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 npm
npm 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 npm
npm install ethereum-waffle ethers --save-dev

La structure de votre projet devrait ressembler à ceci :

1.
2├── contracts
3├── package.json
4└── 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;
2
3interface IERC20 {
4 function balanceOf(address account) external view returns (uint256);
5}
6
7contract AmIRichAlready {
8 IERC20 private tokenContract;
9 uint public richness = 1000000 * 10 ** 18;
10
11 constructor (IERC20 _tokenContract) public {
12 tokenContract = _tokenContract;
13 }
14
15 function check() public view returns (bool) {
16 uint balance = tokenContract.balanceOf(msg.sender);
17 return balance > richness;
18 }
19}
Afficher tout
Copier

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"
9
10import IERC20 from "../build/IERC20.json"
11import AmIRichAlready from "../build/AmIRichAlready.json"
12
13use(solidity)
14
15describe("Am I Rich Already", () => {
16 let mockERC20: Contract
17 let contract: Contract
18 let wallet: Wallet
19
20 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 :

  1. Nous avons fixé notre contrat fictif ERC20 pour toujours retourner le solde de 999999 jetons.
  2. Vérifiez si la méthode contract.check() retourne false.

Nous sommes prêts à allumer la bête :

Un test réussi

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.balanceOf
3 .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.balanceOf
3 .withArgs(wallet.address)
4 .returns(utils.parseEther("1000001"))
5 expect(await contract.check()).to.be.equal(true)
6})

Vous exécutez les tests...

Deux tests réussis

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.balanceOf
3 .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 :

Trois tests réussis

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: @MATsxm(opens in a new tab), 27 février 2024

Ce tutoriel vous a été utile ?