Testing con Hardhat
A continuación, se presenta un ejemplo completo de cómo escribir y ejecutar pruebas unitarias para un contrato inteligente en Ethereum utilizando Hardhat. Este ejemplo incluirá la creación del contrato inteligente, la configuración de los fixtures, y la escritura de diversas pruebas.
Paso 1: Configuración del proyecto
Debemos tener instalado Hardhat y haber creado un proyecto básico (si no sabes cómo hacerlo, revisar el módulo 4).
Asegúrate de tener instalado el plugin
hardhat-toolbox
npm install --save-dev @nomicfoundation/hardhat-toolbox
Y también debes añadir la siguiente línea al inicio de tu fichero
hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
Paso 2: Definir el contrato a probar
Utilizaremos el contrato Token.sol
que ubicaremos en la carpeta contracts
con el siguiente contenido:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Token {
string public name = "KIPU";
string public symbol = "KIP";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * (10 ** uint256(decimals));
balanceOf[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(_to != address(0), "Invalid address");
require(balanceOf[msg.sender] >= _value, "Insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
Paso 3: Escribir las pruebas
Crea un archivo de pruebas llamado TokenTest.js
en la carpeta test
con el siguiente contenido:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
describe("Token contract", function () {
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const initialSupply = 1000n; // Use BigInt
const token = await Token.deploy(initialSupply);
return { token, owner, addr1, addr2 };
}
it("Should set the right owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
expect(await token.balanceOf(owner.address)).to.equal(1000n * 10n ** 18n); // Use BigInt
});
it("Should assign the total supply of tokens to the owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await token.transfer(addr1.address, 50n * 10n ** 18n); // Use BigInt
expect(await token.balanceOf(addr1.address)).to.equal(50n * 10n ** 18n); // Use BigInt
await token.connect(addr1).transfer(addr2.address, 50n * 10n ** 18n); // Use BigInt
expect(await token.balanceOf(addr2.address)).to.equal(50n * 10n ** 18n); // Use BigInt
expect(await token.balanceOf(addr1.address)).to.equal(0n); // Use BigInt
});
it("Should fail if sender doesn’t have enough tokens", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1n * 10n ** 18n) // Use BigInt
).to.be.revertedWith("Insufficient balance");
expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
it("Should update balances after transfers", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await token.transfer(addr1.address, 100n * 10n ** 18n); // Use BigInt
await token.transfer(addr2.address, 50n * 10n ** 18n); // Use BigInt
const finalOwnerBalance = await token.balanceOf(owner.address);
expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150n * 10n ** 18n); // Use BigInt
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(100n * 10n ** 18n); // Use BigInt
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50n * 10n ** 18n); // Use BigInt
});
});
});
Es importante explicar el script anterior.
Hacemos las importaciones y configuraciones que requerimos.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
chai: Una biblioteca de afirmaciones (assertions) que facilita la verificación de los resultados esperados en las pruebas.
ethers: Una biblioteca para interactuar con contratos inteligentes y la blockchain.
loadFixture: Una función que carga un estado inicial fijo para las pruebas, asegurando que todas las pruebas comiencen con las mismas condiciones.
Luego definimos la fixture
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const initialSupply = 1000n; // Usamos BigInt para evitar desbordamientos
const token = await Token.deploy(initialSupply);
return { token, owner, addr1, addr2 };
}
deployTokenFixture: Esta función despliega el contrato
Token
con un suministro inicial. También obtiene tres cuentas para usar en las pruebas (owner, addr1, addr2).
Primera prueba: Verificar el propietario
it("Should set the right owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
expect(await token.balanceOf(owner.address)).to.equal(1000n * 10n ** 18n);
});
Propósito: Verificar que el propietario inicial (la cuenta que despliega el contrato) reciba el suministro total de tokens.
Proceso:
Desplegar el contrato utilizando la fixture.
Obtener el balance del propietario.
Verificar que el balance sea igual al suministro inicial (1000 tokens convertidos a la unidad mínima).
Segunda prueba: Verificar el suministro total
it("Should assign the total supply of tokens to the owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
Propósito: Asegurarse de que el suministro total de tokens se asigna correctamente al propietario.
Proceso:
Desplegar el contrato utilizando la fixture.
Obtener el balance del propietario.
Verificar que el balance del propietario sea igual al suministro total de tokens.
Tercera prueba: Transferir tokens
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await token.transfer(addr1.address, 50n * 10n ** 18n);
expect(await token.balanceOf(addr1.address)).to.equal(50n * 10n ** 18n);
await token.connect(addr1).transfer(addr2.address, 50n * 10n ** 18n);
expect(await token.balanceOf(addr2.address)).to.equal(50n * 10n ** 18n);
expect(await token.balanceOf(addr1.address)).to.equal(0n);
});
Propósito: Verificar que los tokens se transfieren correctamente entre cuentas.
Proceso:
Desplegar el contrato utilizando la fixture.
Transferir 50 tokens de
owner
aaddr1
.Verificar que
addr1
haya recibido 50 tokens.Transferir 50 tokens de
addr1
aaddr2
.Verificar que
addr2
haya recibido 50 tokens y queaddr1
tenga un balance de 0 tokens.
Cuarta prueba: Verificar que una cuenta no transfiere si no tienen fondos suficientes
it("Should fail if sender doesn’t have enough tokens", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1n * 10n ** 18n)
).to.be.revertedWith("Insufficient balance");
expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
Propósito: Asegurarse de que la transferencia falla si el remitente no tiene suficientes tokens.
Proceso:
Desplegar el contrato utilizando la fixture.
Intentar transferir 1 token desde
addr1
(que no tiene tokens) alowner
.Verificar que la transacción sea revertida con el mensaje "Insufficient balance".
Verificar que el balance del
owner
no haya cambiado.
Quinta prueba: Actualización de balances después de transferencia
it("Should update balances after transfers", async function () {
const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await token.balanceOf(owner.address);
await token.transfer(addr1.address, 100n * 10n ** 18n);
await token.transfer(addr2.address, 50n * 10n ** 18n);
const finalOwnerBalance = await token.balanceOf(owner.address);
expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150n * 10n ** 18n);
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(100n * 10n ** 18n);
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50n * 10n ** 18n);
});
Propósito: Verificar que los balances se actualicen correctamente después de las transferencias.
Proceso:
Desplegar el contrato utilizando la fixture.
Transferir 100 tokens de
owner
aaddr1
.Transferir 50 tokens de
owner
aaddr2
.Verificar que el balance final del
owner
sea el balance inicial menos 150 tokens.Verificar que
addr1
tenga 100 tokens yaddr2
tenga 50 tokens.
Paso 4: Ejecutar las pruebas
Para ejecutar las pruebas, utiliza el siguiente comando:
npx hardhat test
Este comando ejecutará todas las pruebas definidas en el archivo TokenTest.js
y mostrará los resultados en la consola.
Si las pruebas han sido exitosas, obtendrás un resultado de este tipo:

¡¡Felicitaciones!! Has ejecutado tus primeras pruebas de forma exitosa. Ahora empieza a hacer pruebas con otros contratos que hayas creado.
Hardhat también te permite obtener la cobertura de tus pruebas, para ello solo necesitas ingresar el siguiente comando:
npx hardhat coverage
y obtendrás un reporte como este

Una cosa más: Chai
En este ejemplo hemos utilizado Chai para realizar nuestras pruebas. Chai es una biblioteca de assertions (afirmaciones) que existe para Node.js que se puede combinar con cualquier framework de pruebas como Hardhat. En el contexto de pruebas para contratos inteligentes, Chai se utiliza ampliamente debido a su sintaxis amigable y capacidades de aserción robustas.
Para usar Chai en tus pruebas, primero necesitas requerir la biblioteca y luego utilizar sus métodos en tus pruebas. Aquí hay un ejemplo básico de configuración:
const { expect } = require("chai");
Chai ofrece tres estilos de aserción principales:
Assert: Estilo clásico basado en funciones.
Expect: Estilo BDD (Behavior-Driven Development) que es más legible.
Should: Estilo BDD que añade propiedades al objeto Object.prototype.
En nuestro ejemplo utilizamos el estilo expect
, ya que es uno de los más utilizados en pruebas de contratos inteligentes con Hardhat.
Principales Comandos de Chai
Igualdad (
equal
,eql
)
equal
verifica igualdad estricta (===
).eql
verifica igualdad profunda para objetos.
expect(1).to.equal(1);
expect({ foo: 'bar' }).to.eql({ foo: 'bar' });
Booleanos (
true
,false
)
Verifica si un valor es verdadero o falso.
expect(true).to.be.true;
expect(false).to.be.false;
Existencia (
exist
)
Verifica si un valor no es null
ni undefined
.
let foo = 'bar';
expect(foo).to.exist;
Tipos (
a
,an
)
Verifica el tipo de un valor.
expect('foo').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
Contenido (
include
,contain
)
Verifica si un valor contiene otro valor (en arrays, strings, o objetos).
expect([1, 2, 3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', baz: 'qux' }).to.include({ foo: 'bar' });
Longitud (
lengthOf
)
Verifica la longitud de un array, string, o Map.
expect([1, 2, 3]).to.have.lengthOf(3);
expect('foo').to.have.lengthOf(3);
Mayor y menor que (
above
,below
)
Verifica si un valor es mayor o menor que otro.
expect(10).to.be.above(5);
expect(5).to.be.below(10);
Cerca de (
closeTo
)
Verifica si un número está cerca de otro, con un margen de error.
expect(1.5).to.be.closeTo(1.4, 0.1);
Emisión de eventos (
emit
,withArgs
)
En el contexto de pruebas de contratos inteligentes, verificamos si un evento se emite.
await expect(contract.emitEvent()).to.emit(contract, "EventName").withArgs(expectedArgs);
Para mayor referencia de Chai, revisa la documentación de Hardhat.
Last updated
Was this helpful?