Waffle: Llamadas dinámicas de simulación y prueba de contratos
¿De qué trata este tutorial?
En este tutorial aprenderás como:
- utilizar simulación dinámica
- comprobar las interacciones entre contratos inteligentes
Supuestos:
- ya sabes como escribir un contrato inteligente simple en
Solidity
- ya sabes utilizar
JavaScript
yTypeScript
- ya has hecho otrod tutoriales de
Waffle
o sabes algunas cosas sobre él
Simulación dinámica
¿Por qué es útil la simulación dinámica? Bueno, porque nos permite escribir pruebas unitarias en lugar de pruebas de integración. ¿Y eso, qué significa? Significa que no tenemos que preocuparnos por las dependencias de los contratos inteligentes, por lo que podremos probarlos de forma aislada. Déjame mostrarte cómo puedes hacerlo.
1. Proyecto
Antes de comenzar debemos preparar un proyecto simple node.js:
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# or if you're using npmnpm init
Comencemos agregando dependencias de typescript y prueba - 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
Ahora agreguemos Waffle
y ethers
:
yarn add --dev ethereum-waffle ethers# or if you're using npmnpm install ethereum-waffle ethers --save-dev
La estructura de tu proyecto debería verse así:
1.2├── contracts3├── package.json4└── test
2. Contrato inteligente
Para comenzar una simulación dinámica, necesitamos un contrato inteligente con dependencias. No te preocupes, ¡yo me encargo!
Aquí hay un contrato inteligente simple escrito en Solidity
cuyo único proposito es comporbar si somos ricos. Utiliza el token ERC20 para comprobar si tenemos suficientes tokens. Ponlo en ./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}Mostrar todoCopiar
Como queremos utilizar la simulación dinámica no necesitamos el ERC20 completo, por eso estamos utilizando la interfaz de IERC20 con sólo una función.
¡Es hora de construir este contrato! Para ello utilizaremos Waffle
. Primero, debemos crear un archivo de configuración simple waffle.json
que especifique las opciones de compilación.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}Copiar
Ahora estamos listos para crear el contrato con Waffle:
npx waffle
Fácil, ¿verdad? En la carpeta build/
aparecieron dos archivos correspondientes al contrato y la interfaz. Los utilizaremos luego para las pruebas.
3. Pruebas
Creemos un archivo llamado AmIRichAlready.test.ts
para estas pruebas. Antes que nada, tenemos que gestionar las importaciones. Las necesitaremos luego:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"
Excepto para las dependencias JS, necesitaremos importar o crear el contrato y la interfaz:
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"
Waffle utiliza chai
para las pruebas. Sin embargo, antes de utilizarlo, debemos insertar los emparejadores de Waffle en el propio chai:
1use(solidity)
Necesitamos implementar una función beforeEach()
que restablezca el estado del contrato antes de cada prueba. Pensemos primero en lo que necesitamos allí. Para implementar un contrato necesitaremos dos cosas: un monedero y un contrato ERC20 ya implementado para utilizarlo como argumento del contrato AmIRichAlready
.
Primero creamos el monedero:
1const [wallet] = new MockProvider().getWallets()
Luego debemos desplegar un contrato ERC20. Aquí está la parte difícil - sólo tenemos una interfaz. Esta es la parte en que Waffle viene a salvarnos. Waffle tiene una función mágica deployMockContract()
que crea un contrato usando únicamente el abi de la interfaz:
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)
Ahora con el monedero y el ERC20 desplegado, podemos continuar con la implementación del contrato AmIRichAlready
:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])
Con todo eso, nuestra función beforeEach()
está terminada. Hasta aquí, tu archivo AmIRichAlready.test.ts
debería verse así:
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})Mostrar todo
Escribamos la primera prueba para el contrato AmIRichAlready
. ¿De qué crees que debería ser nuestra prueba? ¡Sí, tienes razón! Deberíamos comprobar si ya somos ricos :)
Pero espera un segundo. ¿Cómo sabrá nuestro contrato simulado que valores devolver? No hemos implementado ninguna lógica para la función balanceOf()
. Nuevamente, Waffle nos puede ayudar. Nuestro contrato simulado tiene algunas cosas nuevas:
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)
Con este conocimiento podemos, finalmente, escribir nuestra primera prueba:
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})
Separemos esta prueba en partes:
- Establecimos nuestro contrato ERC20 para devolver siempre un balance de 999999 tokens.
- Comprobar si el método
contract.check()
devuelvefalse
.
Estamos listos para liberar a la bestia:
Así que la prueba funciona, pero... todavía hay margen de mejora. La función balanceOf()
siempre devolverá 999999. Podemos mejorarla especificando un monedero para el cual la función devolverá algo, como un contrato real:
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})
Hasta el momento, sólo probamos el caso donde aún no somos suficientemente ricos. Probemos el opuesto esta vez:
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})
Ejecutas las pruebas...
... ¡y aquí está! Nuestro contrato parece funcionar según lo previsto :)
Probando llamadas de contrato
Veamos lo que hicimos hasta ahora. Probamos la funcionalidad de nuestro contrato AmIRichAlready
y parece estar funcionando correctamente. Esto significa que terminamos, ¿verdad? ¡No exactamente! Waffle nos permite probar nuestro contrato aún más. ¿Pero cómo? Bueno, en el arsenal de Waffle tenemos calledOnContract()
y los matchers calledOnContractWith()
. Siempre nos permitirán corroborar si nuestro contrato llamó al contrato simulado ERC20. Aquí hay una prueba básica con uno de estos matchers:
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})
Incluso podemos ir más allá y mejorar esta prueba con los otros matchers de los que te hablé:
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})
Vamos a comprobar si las pruebas fueron correctas:
Genial, todas las pruebas están verdes.
Probar las llamadas de contrato con Waffle es muy fácil. Y aquí está la mejor parte. ¡Estos emparejadores trabajan tanto con contratos normales como simulados! Esto se debe a que Waffle registra y filtra las llamadas EVM en lugar de introducir código, como en el caso de las librerías de prueba populares para otras tecnologías.
La Línea de Llegada
¡Felicidades! Ahora sabes como usar Waffle para probar las llamadas de contrato y contratos simulados de forma dinámica. Hay características mucho más interesantes que descubrir. Recomiendo revisar la documentación de Waffle.
La documentación de Waffle está disponible aquí(opens in a new tab).
El código fuente de este tutorial puedes econtrarlo aquí(opens in a new tab).
Otros tutoriales que podrían interesarte:
Última edición: @nhsz(opens in a new tab), 27 de febrero de 2024