Ir al contenido principal

Waffle: Llamadas dinámicas de simulación y prueba de contratos

wafflecontratos inteligentessoliditypruebassimular
Intermedio
Daniel Izdebski
14 de noviembre de 2020
6 minuto leído minute read

¿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 y TypeScript
  • 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-mocking
cd dynamic-mocking
mkdir contracts src
yarn init
# or if you're using npm
npm 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 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
# or if you're using 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 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;
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
Copiar

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

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

Estamos listos para liberar a la bestia:

Pasando una prueba

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.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("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})

Ejecutas las pruebas...

Pasando dos 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.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:

Pasando tres pruebas

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:

  • Probar contratos inteligentes con Waffle

Última edición: @nhsz(opens in a new tab), 27 de febrero de 2024

¿Le ha resultado útil este tutorial?