¿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:
Preparación del entorno: Instalar las herramientas necesarias como Hardhat, y los plugins de Ethers.js si no lo has hecho aún.
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.
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",asyncfunction(){const maxSupply =4000;const{ deploy }=awaitsetup(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",asyncfunction(){const{ deploy, owner }=awaitsetup();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",asyncfunction(){const maxSupply =2;const{ deploy }=awaitsetup(maxSupply);awaitPromise.all([deploy.mint(), deploy.mint()]);awaitexpect(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.
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!
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(){constsetup=async(_maxSupply =2, tokenId =1)=>{constPlatziPunks=await ethers.getContractFactory("XPlatziPunks");const platzi_punks =awaitPlatziPunks.deploy(_maxSupply);await platzi_punks.deployed();const[owner]=await ethers.getSigners();constPseudoRandomDNA=await platzi_punks.deterministicPseudoRandomDNA(tokenId, owner.address)return{ platzi_punks, owner,PseudoRandomDNA}}let platzi_punks;let owner;letPseudoRandomDNA;beforeEach(async()=>{const test_utils =awaitsetup(); platzi_punks = test_utils.platzi_punks owner = test_utils.ownerPseudoRandomDNA= 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}=awaitsetup(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}=awaitsetup(2);try{// Exceeds mintingawait 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(asyncfunction(){constPunkDNA=await ethers.getContractFactory("XPunkDNA"); punk_dna =awaitPunkDNA.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);})});
Hola Andres! Tengo una duda, que significa la x antes de los metodos en de las pruebas?
por ejemplo "x_baseURI"
Intento aplicarla pero me marca que no es una funcion, acaso asi con la X llamaste a tus metodos?
Buenas Andres!!
Buen apunte lo de esa libreria
Te compila el proyecto añadiendola? Porque a mi me peta si hago como indican en la documentacion
hardhat compile --force
Y parece que se lia con la internal variable de la lib Base64
'DeclarationError: Undeclared identifier. Did you mean "$TABLE"?
--> contracts-exposed/Base64.sol:11:16:
|
11 | return TABLE;
'
Para hacer un mint en mainnet se paga gas, pero si quisiéramos generar una colección de 10.000 NFT como se hace hoy en día, deberíamos pagar este gas x 10.000? No hay manera de crear todos con una sola transacción?
Como se puede hacer esto?
Hola, para hacer una colección de 10 k de ERC-721 independientemente de para que los quieras, se hacen con un contrato, que permite establecer el limite de tokens generados hasta 10 k . Se paga el gas del contrato y quienes mintean y creen en tu proyecto pagan su gas y el precio tu le pongas a tus NFTs por el minteo.
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
en mi caso no fue necesario :D
Tomo su tiempo, pero se logró.
¿Cómo resolviste el problema de Minting has a limit? lo he intentado resolver por días y no puedo solucionarlo
Mi problema era el minting, y era porque en el ".revertedWith('No PlatziPunks left :()" tiene que tener el mismo texto que en 'require(current < maxSupply, "No PlatziPunks left :(")'.
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.
3 horas después de ver el mismo error, vi este comentario y todo tuvo sentido. Gracias, me salvaste de un paro cardiaco jajaja.
Aun estoy viendo que sucedió acá
PlatziPunksContractDeployment ✓ Sets max suplly to passed param(1554ms)Minting ✓ Mints a newtoken and assing it to owner(653ms)1)Has a minting limit
2passing(3s)1 failing
1)PlatziPunksContractMintingHas a minting limit:AssertionError:Expected transaction to be reverted
+ expected - actual
-TransactionNOT reverted.+Transaction reverted.
Disculpe tengo este error cuando se ejecuta la tercera prueba, en si es la unica que no pasa, ya corregi el error que se ve en el video "_idCounter.increment()" pero aun asi me sale el siguiente error.
¿A que se debe este error?.
1)PlatziPunksContractMintingHas a minting limit:AssertionError:Expected transaction to be reverted
+ expected - actual
-TransactionNOT reverted.+Transaction reverted.
Hola David,
Por lo que veo, no está aplicando el limite porque la transacción no revierte.
Utiliza el console.log de hardhat para debuggear el limit que se coloca
Ayuda.. tengo el siguiente error de test... alguien podría explicarme como solucionarlo ?? 🙃
Escribiendo exactamente el código de ejemplo que hace el profesor me figura el error:
TypeError: deployed.maxSupply is not a function
Cuando ejecuto
npx hardhat test
Hola, una consulta, tengo un problema con la ultima prueba, este es el error que obtengo "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined"
Hola, me podrian ayduar por favor, tengo dos errores
Tengo el mismo error que David Almeyda :( , lo comente para continuar con la prueba del tokenURI y me da el siguiente error en tokenURI
Error: invalid codepoint at offset 63; unexpected continuation byte (argument="bytes", value=Uint8Array( Aqui aparece un numero enorme ), code=INVALID_ARGUMENT, version=strings/5.5.0)
Espero me puedan ayudar, gracias.
Saludos
El número grande es el buffer de datos, seguramente estás teniendo algún problema en la conversión de datos en algún punto, intenta ir haciendo console.log en cada paso para determinar qué falla.
Similar a la clase, mi error fue insertar un caracter no ascii al no convertir el tokenId a string.
Puede ser algo similar
Gracias Ernesto, coloque los console.log y por alguna extraña razon al concatenar el data con el json base64 aparecian algunos caracteres ( @�↔data:application/json;base64,☻TeyA... ), encontre esta pagina donde implementan un poco diferentes las cosas y me soluciono el problema.
Saludos
Hola, una pregunta, alguien sabe como podemos testear la función de Enumerable?
Para mintear un token uso la función deployedContract.mint(), pero ¿cómo puedo hacer para cambiar el firmante de la transaccion? Esto es, para mintear un token desde una dirección diferente
a alguien le sucede que deployed.maxSupply() is not a function?
Ahi lo pude resolver el mismo error que vos, el problema es q me confundi cuando configure el setup y en el return de ese meodo le puse parentesis en vez de llaves con eso solo se soluciono el problema.
En mi caso el problema es que tenía a maxSupply como inmutable en el contract, tenés que cambiarlo a public:
uint256 public maxSupply;
Por si a alguien le ayuda
Al realizar el npx hardhat test aparece este error:
1)PlatziPunksContract tokenURI
returns valid metadata:SyntaxError:Unexpected token h inJSON at position 146 at JSON.parse(<anonymous>) at Context.<anonymous>(test\platzipunks.js:79:35)
estoy como tu
Duda, existe algun equivalente en solidity de JSON.stringify?, se me hace engorroso manipular la string JSON directamente
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