No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Probando nuestro Smart Contract

21/24
Recursos

¿Cómo escribir pruebas en Smart Contracts con Hardhat?

¡Bienvenido al fascinante mundo de las pruebas para Smart Contracts! Ahora que tienes tu Smart Contract de PlatziPunks con las funcionalidades completas, es crucial asegurarse de que todo funcione correctamente antes de avanzar al despliegue en una red de prueba. Vamos a enfocar nuestras energías en crear tests usando Hardhat y JavaScript, que son herramientas esenciales en este proceso.

¿Por qué es importante realizar pruebas?

Las pruebas son fundamentales para garantizar que nuestro Smart Contract funcione de manera adecuada y segura. Algunos beneficios importantes de realizar pruebas incluyen:

  • Identificación de errores: Facilitan la detección de errores y malfuncionamientos antes de desplegar el contrato.
  • Validación de funcionalidad: Aseguran que todas las funciones implementadas operen conforme a las expectativas.
  • Mantenimiento de la calidad del código: Reducen el riesgo de errores futuros a medida que se hacen cambios o mejoras en el código.

¿Cómo empezar con las pruebas en Hardhat?

Para comenzar con las pruebas en Hardhat necesitas seguir algunos pasos iniciales:

  1. Preparación del entorno: Instalar las herramientas necesarias como Hardhat, y los plugins de Ethers.js si no lo has hecho aún.
  2. Crear el archivo de pruebas: Dentro de la carpeta de pruebas, crea un archivo platzipunks.js, donde escribirás todos los tests para tu contrato.
  3. Importar las utilidades de Chai: Principalmente la función expect de Chai, que te ayudará a comparar los resultados reales con los esperados.
const { expect } = require("chai");

¿Cómo estructurar las pruebas?

Las pruebas generalmente se estructuran en suites que agrupan test relacionados. Usamos describe para definir una suite de pruebas, mientras que it define pruebas individuales.

Prueba de Max Supply

Vamos a crear una prueba básica para verificar que el max supply funciona correctamente.

it("set max supply to pass param", async function () {
    const maxSupply = 4000;
    const { deploy } = await setup(maxSupply);
    const returnedMaxSupply = await deploy.maxSupply();
    expect(maxSupply).to.equal(returnedMaxSupply);
});

Aquí, nos aseguramos que el suministro máximo configurado en el contrato sea igual al que hemos pasado como parámetro.

Prueba de Minting

La siguiente prueba verifica que al ejecutar la función de mint, se cree un nuevo token y se asigne al dueño del contrato.

it("mint a new token and assign it to owner", async function () {
    const { deploy, owner } = await setup();
    await deploy.mint();
    const ownerOfMinted = await deploy.ownerOf(0);
    expect(ownerOfMinted).to.equal(owner.address);
});

Esta prueba garantiza que el token mintado pertenece al dueño correcto.

Límite de Minting

Probamos también que no se muevan tokens más allá del límite del max supply.

it("has a minting limit", async function () {
    const maxSupply = 2;
    const { deploy } = await setup(maxSupply);
    await Promise.all([deploy.mint(), deploy.mint()]);
    await expect(deploy.mint()).to.be.revertedWith("PlatziPunks: max supply reached");
});

Esta prueba asegura que no se pueda crear más tokens una vez alcanzado el límite de max supply.

Pruebas del token URI

Finalmente, es importante verificar que el token URI retorna metadatos válidos.

describe("token URI", function () {
    it("returns valid metadata", async function () {
        const { deploy } = await setup();
        await deploy.mint();
        const tokenURI = await deploy.tokenURI(0);
        const [prefix, base64JSON] = tokenURI.split('base64,');
        const metadata = JSON.parse(Buffer.from(base64JSON, 'base64').toString('ascii'));
        expect(metadata).to.include.keys('name', 'description', 'image');
    });
});

Esto nos asegura que la metadata del token está correctamente formateada y contiene la información necesaria.

Recomendaciones finales

  • Itera en tus pruebas: Usa este enfoque para probar todas las funcionalidades de tu contrato a medida que añades nuevas características.
  • Documentación y plugins: Familiarízate con la documentación de Hardhat, Chai y Ethers.js para maximizar el uso de sus capacidades en tus pruebas.

Las pruebas continuas no solo fortalecen tu contrato, sino que también construyen una confianza sólida en el desenvolvimiento de tus recursos en la red. ¡Continúa experimentando y aprendiendo para mejorar cada vez más tus dApps y Smart Contracts!

Aportes 20

Preguntas 12

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Aquí están los test que hice. Use una librería que se llama hardhat-exposed https://github.com/frangio/hardhat-exposed que hace que funciones que están no visibles se vuelvan publicas para poder testear me gusto la idea pero creo la lib aun no esta lista para producción igual no se si será buena practica hacer esto

const { expect } = require("chai");

describe("Platzi Punks Contract", function () {
  const setup = async (_maxSupply = 2, tokenId = 1) => {
    const PlatziPunks = await ethers.getContractFactory("XPlatziPunks");
    const platzi_punks = await PlatziPunks.deploy(_maxSupply);
    await platzi_punks.deployed();
    
    const [owner] = await ethers.getSigners();
    const PseudoRandomDNA = await platzi_punks.deterministicPseudoRandomDNA(tokenId, owner.address)
    
    return {
      platzi_punks,
      owner,
      PseudoRandomDNA
    }
  }

  let platzi_punks;
  let owner;
  let PseudoRandomDNA;
  beforeEach(async () => {
    const test_utils = await setup();
    platzi_punks = test_utils.platzi_punks
    owner = test_utils.owner
    PseudoRandomDNA = test_utils.PseudoRandomDNA
  })

  describe("Deployment", () => {
    it("Should init contract with name and symbol", async () => {
      expect(await platzi_punks.name()).to.equal("PlatziPunks");
      expect(await platzi_punks.symbol()).to.equal("PLPKS");
    });

    it("Should init max supply with pass params", async () => {
      const maxSupply = 150;
      const {platzi_punks} = await setup(maxSupply);
      expect(await platzi_punks.maxSupply()).to.equal(maxSupply);
    });
  })

  describe("Minting", async () => {
    it("Mints a new token and assigns it to owner", async () => {
      await platzi_punks.mint();
      expect(await platzi_punks.ownerOf(1)).to.equal(owner.address);
    });

    it("Has a minting limit", async () => {      
      const {platzi_punks} = await setup(2);

      try {
        // Exceeds minting
        await platzi_punks.mint()
        await platzi_punks.mint()
        await platzi_punks.mint()
        expect.fail('fail with an error');
      } catch (error) {
        expect(error.message).to.contains('No Platzi Punks left :(');
      }
    });

    it("Should increment the balanceof the owner by 1 every mint", async () => {
      expect(await platzi_punks.balanceOf(owner.address)).to.equal(0);
      await platzi_punks.mint()
      await platzi_punks.mint()
      expect(await platzi_punks.balanceOf(owner.address)).to.equal(2);
    });

    it("Should increment tokenId in every mint", async () => {
      await platzi_punks.mint()
      expect(await platzi_punks.ownerOf(1)).to.equal(owner.address);
      await platzi_punks.mint()
      expect(await platzi_punks.ownerOf(2)).to.equal(owner.address);
    });

    it("Should save token dna on DnaByToken", async () => {
      await platzi_punks.mint()
      expect(await platzi_punks.DnaByToken(1).toString().length).to.equal(16);
      expect(await platzi_punks.DnaByToken(1)).to.equal(PseudoRandomDNA);
    });
  }) 

  describe("tokenURI", async () => {
    it("Should throw an error if tokenId don't exists", async () => {
      try {
        await platzi_punks.tokenURI(1)
        expect.fail('fail with an error');
      } catch (error) {
        expect(error.message).to.contains('ERC721Metadata: URI query for nonexistent token');
      }
    });

    it("Should have tokenURI correct metadata", async () => {
      await platzi_punks.mint()

      const tokenURI = await platzi_punks.tokenURI(1)
      const [ prefix, base64JSON ] = tokenURI.split(',')
      const stringifiedMetaData = Buffer.from(base64JSON, 'base64').toString('ascii');
      const metadata = JSON.parse(stringifiedMetaData)

      expect(prefix).to.equal("data:application/json;base64");
      expect(metadata).to.have.all.keys("name", "description", "image")

      expect(metadata.name).to.includes('PlatziPunks #1');
      expect(metadata.image).to.includes('https://avataaars.io/?')
      expect(metadata.image).to.includes('accessoriesType=')
      expect(metadata.image).to.includes('topType=')
    });

    it("Should have _baseUri avataaars.io", async () => {
      expect(await platzi_punks.x_baseURI()).to.equal('https://avataaars.io/');
    });

    it("Should get _paramsURI", async () => {
      expect(await platzi_punks.x_paramsURI(PseudoRandomDNA)).to.contains('accessoriesType=');
      expect(await platzi_punks.x_paramsURI(PseudoRandomDNA)).to.contains('topType=');
    });

    it("Should get imageByDNA", async () => {
      expect(await platzi_punks.imageByDNA(PseudoRandomDNA)).to.contains('https://avataaars.io/?');
      expect(await platzi_punks.imageByDNA(PseudoRandomDNA)).to.contains('accessoriesType=');
      expect(await platzi_punks.imageByDNA(PseudoRandomDNA)).to.contains('topType=');
    });
  })
});

-------------------------------------------------------------------

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("PunkDNA", function () {
  let punk_dna;
  beforeEach(async function() {
    const PunkDNA = await ethers.getContractFactory("XPunkDNA");
    punk_dna = await PunkDNA.deploy();
    await punk_dna.deployed();
  });

  it('should get DNA section', async() => {
    expect(await punk_dna.x_getDNASection(87654321, 0)).to.equal(21);
    expect(await punk_dna.x_getDNASection(87654321, 1)).to.equal(32);
    expect(await punk_dna.x_getDNASection(87654321, 2)).to.equal(43);
    expect(await punk_dna.x_getDNASection(87654321, 3)).to.equal(54);
    expect(await punk_dna.x_getDNASection(87654321, 4)).to.equal(65);
    expect(await punk_dna.x_getDNASection(87654321, 6)).to.equal(87);
    expect(await punk_dna.x_getDNASection(87654321, 8)).to.equal(0);
  })

  it('should get deterministic PseudoRandom DNA', async() => {
    const [signer] = await ethers.getSigners();
    expect(await punk_dna.deterministicPseudoRandomDNA(1, signer.address)).to.be.an('object');
    expect(await punk_dna.deterministicPseudoRandomDNA(1, signer.address).toString().length).to.equal(16);
  })
});

Hacer pruebas es una buena practica que ayuda a detectar errores de funcionamiento y verificar que nuestro smart Contract funcione como se espera.

Definitivamente

import "hardhat/console.sol";

me ayudo a encontrar el error, pero lo ideal es que eviten copiar la current <= maxSupply y mantengan el <

Si a

"data:application/json;base64,"

dejas la coma al final de base64, el test tira error. Por lo menos a mi me fallaba y le tuve que sacar la coma


Tomo su tiempo, pero se logró.

Cuando intente usar la libreria de Strings de openzepeling con el siguiente codigo, me generaba un error

tokenId.toString()

Para solucionarlo cambie le definicio del framento de codigo con lo siguiente.

Strings.toString(tokenId)

Espero que les ayude si tienen el mismo problema que yo tuve.

Yo utilicé un for y así hice un poco más dinámico el test

for(let i=0; i<maxSupply; i++){
	await deployed.mint()
 }

si a alguno le genera error el revertedWith verifiquen que el string que uses para el error en el smart contract sea el mismo string que usen en revertedWith de lo contrario generara error

Así sería testear el “Minting” con diferentes usuarios:

it("Mints a new token and assigns it to owner", async () => {
      const { deployed } = await setup({});
      const [owner, user1, user2] = await ethers.getSigners();

      await deployed.mint();
      await deployed.connect(user1).mint();
      await deployed.connect(user2).mint();

      const ownerOfMinted0 = await deployed.ownerOf(0);
      const ownerOfMinted1 = await deployed.ownerOf(1);
      const ownerOfMinted2 = await deployed.ownerOf(2);

      expect(ownerOfMinted0).to.equal(owner.address);
      expect(ownerOfMinted1).to.equal(user1.address);
      expect(ownerOfMinted2).to.equal(user2.address);
    });

Para los que hicieron el tema de que mint sea payable, hay algunas cosas que deben tener en cuenta para los test:

  1. en el const setup, deben agregar payees y shares_ así como cuando llamen el await setup, les pongo un ejemplo
...
const setup = async ({ maxSupply = 10000, payees = [], shares_ = [100]}) => {
...

 const { deployed } = await setup({ maxSupply, payees: [deployer.address], shares_: [100] });

  1. Cuando quieran hacer mint, deben pasar el value de la siguiente manera:
// El 0.005 cambia por el valor que pusieron en su contrato
await deployed.mint({value: ethers.utils.parseEther("0.005")});
  1. Yo use el tema de ethers.getSigners(); para obtener al deployer para así pasar en payees el deployer.address
describe("tokenURI", () => {
      it("returns valid metadata", async () => {
        const maxSupply = 1000;
        const [deployer] = await ethers.getSigners();
        const { deployed } = await setup({ maxSupply, payees: [deployer.address], shares_: [100] });

De esta forma funcionaron todos mis tests c:

Si como yo convertiste tu función mint en tipo payable, la manera de enviar ether es la siguiente:

await deployed.mint({value: ethers.utils.parseEther('2')});

Las pruebas arrojan un error que indica bug en nuestro código

await Promise.all( ) ???

INDAGAR: to.be.revertedWith('message')

INDAGAR: overwirte del MaxSupply

NVM: Windows: https://content.breatheco.de/en/how-to/nvm-install-windows https://docs.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows

expect: https://www.chaijs.com/guide/styles/#expect https://www.chaijs.com/api/bdd/

Test Suite Medium article: https://blog.bitsrc.io/build-your-own-javascript-testing-framework-377e6583c870 https://gist.github.com/philipszdavido/b44c62cd7bcd94740e33af5859e7810c#file-js-test-lib-index-js pruebas con chai y mocha: https://www.youtube.com/watch?v=-sn3H0V3PuY another one: https://codeburst.io/how-to-test-javascript-with-mocha-the-basics-80132324752e

https://www.chaijs.com/ also https://mochajs.org/

En el material de ayuda en la en donde se hace el split, se escribió mal la separación,
Tiene;

 "data:application/json;base64,"

Cuando debería ser;

"data:aplication/json;base64,"

Por si lo copiaron y no funciona haha