Ir al contenido principal

Waffle: simulación dinámica y pruebas de llamadas a contratos

waffle
contratos Inteligentes
Solidity
pruebas
simular
Intermedio
Daniel Izdebski
14 de noviembre de 2020
7 minuto leído

¿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 JavaScript y TypeScript
  • ya has hecho otros tutoriales de Waffle o 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-dinamica
cd simulacion-dinamica
mkdir contracts src
yarn init
# o si estás usando npm
npm init

Comencemos 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 npm
npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev

Ahora agreguemos Waffle y ethers:

yarn add --dev ethereum-waffle ethers
# o si estás usando npm
npm install ethereum-waffle ethers --save-dev

La estructura de tu proyecto debería verse así:

1.
2├── contracts
3├── package.json
4└── 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 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;
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}
Mostrar todo

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, 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 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"

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"
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})
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 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:

  1. Establecimos nuestro contrato ERC20 simulado para devolver siempre un saldo de 999999 tokens.
  2. Comprobar si el método contract.check() devuelve false.

Estamos listos para liberar a la bestia:

Una prueba superada

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

Ejecutas las pruebas...

Dos pruebas superadas

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

Tres pruebas superadas

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

¿Le ha resultado útil este tutorial?