Waffle: simulación dinámica y pruebas de llamadas a contratos
¿De qué trata este tutorial?
En este tutorial aprenderás cómo:
- utilizar simulación dinámica
- comprobar las interacciones entre contratos inteligentes
Suposiciones:
- ya sabes cómo escribir un contrato inteligente simple en
Solidity - ya sabes utilizar
JavaScriptyTypeScript - ya has hecho otros tutoriales de
Waffleo sabes algunas cosas sobre él
Simulación dinámica
¿Por qué es útil la simulación dinámica? Bueno, 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 simulacion-dinamicacd simulacion-dinamicamkdir contracts srcyarn init# o si estás usando npmnpm initComencemos agregando dependencias de typescript y prueba: mocha y chai:
yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# o si estás usando npmnpm install @types/chai @types/mocha chai mocha ts-node typescript --save-devAhora agreguemos Waffle y ethers:
yarn add --dev ethereum-waffle ethers# o si estás usando npmnpm install ethereum-waffle ethers --save-devLa estructura de tu proyecto debería verse así:
1.2├── contracts3├── package.json4└── test2. 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 propósito es comprobar 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 todoComo 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, vamos a crear un archivo de configuración simple waffle.json que especifica las opciones de compilación.
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}Ahora estamos listos para crear el contrato con Waffle:
npx waffleFá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"Aparte de las dependencias de JS, necesitamos importar nuestro contrato compilado 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 desplegar un contrato, necesitamos dos cosas: una billetera y un contrato ERC20 desplegado para pasarlo como argumento del contrato AmIRichAlready.
Primero, creamos la billetera:
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 la billetera y el ERC20 desplegado, podemos continuar e implementar el 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 todoEscribamos 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 qué 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("devuelve «false» si la billetera tiene menos de 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 simulado para devolver siempre un saldo 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 una billetera para la que la función devolverá algo, como un contrato real:
1it("devuelve «false» si la billetera tiene menos de 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("devuelve «true» si la billetera tiene al menos 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 :)
Prueba de llamadas a contratos
Veamos lo que hicimos hasta ahora. Hemos probado la funcionalidad de nuestro contrato AmIRichAlready y parece que funciona 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 están los emparejadores calledOnContract() y calledOnContractWith(). Nos permitirán comprobar si nuestro contrato llamó al contrato simulado de ERC20. Aquí hay una prueba básica con uno de estos emparejadores:
1it("comprueba si el contrato llamó a balanceOf en el token ERC20", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 await contract.check()4 expect("balanceOf").to.be.calledOnContract(mockERC20)5})Podemos ir aún más lejos y mejorar esta prueba con el otro emparejador del que te hablé:
1it("comprueba si el contrato llamó a balanceOf con una billetera determinada en el token ERC20", 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 funcionan tanto con contratos normales como simulados! Esto se debe a que Waffle registra y filtra las llamadas de la EVM en lugar de inyectar código, como es el caso de las librerías de prueba populares para otras tecnologías.
La recta final
¡Enhorabuena! Ahora sabes cómo 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 se puede encontrar aquíopens in a new tab.
Otros tutoriales que podrían interesarte:
Última actualización de la página: 27 de febrero de 2024


