Aprende a realizar tests con Hardhat


Los tests se utilizan para verificar el comportamiento y las funcionalidades desarrolladas. En Solidity todo esto tiene aún más importancia ya que por definición un contrato es inmutable, por lo que es más que aconsejable que los tests comprueben el comportamiento de los contratos.

En este post aprenderemos a como realizar los tests en el entorno Hardhat.

📰

He creado este un repositorio para poder ver y realizar todos los tests explicados en este post.

GitHub

Eventos

Es necesario poder testear un evento emitido por Solidity porque los eventos son una parte fundamental de la comunicación entre contratos inteligentes y aplicaciones externas. Los eventos permiten que los contratos informen sobre su estado o cualquier cambio relevante que ocurra en la cadena de bloques.

Al testear un evento emitido por Solidity, podemos verificar que el contrato se está comportando correctamente y está emitiendo los eventos esperados en respuesta a las acciones realizadas. Esto nos permite garantizar la integridad y la calidad del contrato, así como verificar que los eventos se están generando correctamente con los datos correctos.

Evento básico:

En este test, se valida si la función callEvent() emite el evento llamado BasicEvent.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;

contract HardhatMatcherTester {
 event BasicEvent();

 function callEvent() external {
  emit BasicEvent();
 }
}
describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if callEvent throw a BasicEvent', async () => {
  await expect(hardhatMatcherTester.callEvent()).to.emit(
   hardhatMatcherTester,
   'BasicEvent'
  )
 })
})

Evento con argumentos

Aparte de poder validar que el evento ha sido emitido correctamente, podemos validar que el valor que está emitiendo es el que nosotros queremos que emita.

También puede ocurrir que no sabemos al 100% o no nos interesa validar que valor que está emitiendo el evento, pero sí queremos validar que emite un evento con un argumento. Para esos casos Hardhat ha creado una función que únicamente valida que hay un argumento.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;

contract HardhatMatcherTester {
 event EventWithArg(address hardhatMatcherTesterAddress);

 function callEventWithArg() external {
  emit EventWithArg(address(this));
 }
}
describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if callEventWithArg emit a EventWithArg event and check the arg', async () => {
  await expect(hardhatMatcherTester.callEventWithArg())
   .to.emit(hardhatMatcherTester, 'EventWithArg')
   .withArgs(await hardhatMatcherTester.getAddress())
 })
})
import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'

describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if callEventWithArg emit a EventWithArg event and check the arg', async () => {
  await expect(hardhatMatcherTester.callEventWithArg())
   .to.emit(hardhatMatcherTester, 'EventWithArg')
   .withArgs(anyValue)
 })
})

Revert

El revert es una instrucción de Solidtiy que se utiliza para manejar errores y condiciones inesperadas dentro de los contratos. Puede ser utilizado para verificar condiciones antes de realizar una transacción, validar entradas de usuario, comprobar saldos suficientes, entre otros casos de uso. Al detectar una situación no deseada, se utiliza el revert para retroceder los cambios y asegurar que el contrato mantenga su estado consistente.

La instrucción revert puede recibir un parámetro opcional, que es un mensaje de error personalizado. Este mensaje se utiliza para proporcionar información adicional sobre la razón del revert y puede ser útil para la depuración y la comunicación con los usuarios del contrato.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;

contract HardhatMatcherTester {
    
 function revertedWithMessage() external pure {
  revert('ERROR_MESSAGE');
 }
}
describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if revert when call reverted function', async () => { 
  await expect(hardhatMatcherTester.revertedWithMessage()).to.be.reverted 
 })
})
describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if revert when call revertedWithMessage function', async () => {
  await expect(hardhatMatcherTester.revertedWithMessage()).to.be.revertedWith('ERROR_MESSAGE')
 })
})

Panic codes

Los Panic Codes (Códigos de Pánico) son una técnica utilizada en los contratos que se emplean para manejar excepciones y errores de manera estructurada en el código.

Cuando ocurre una excepción o un error durante la ejecución de un contrato, se puede utilizar un Panic Code para comunicar de manera clara y concisa el motivo de la falla. Los Panic Codes son números enteros positivos que se asignan a diferentes tipos de errores o situaciones excepcionales. Cada Panic Code representa un caso particular que puede ocurrir durante la ejecución del contrato.

Por ejemplo, se pueden utilizar estructuras de control como condicionales o bloques try-catch para capturar y gestionar los errores de manera adecuada. Esto permite tomar medidas correctivas, como revertir las transacciones, notificar a los usuarios sobre el error o realizar cualquier otra acción requerida según el contexto.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;

contract HardhatMatcherTester {
    
 function panicCode() external pure {
  assert(1 < 0);
 }
}
import { PANIC_CODES } from '@nomicfoundation/hardhat-chai-matchers/panic'

describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check panicCode error', async () => {
  await expect(hardhatMatcherTester.panicCode()).to.be.revertedWithPanic( PANIC_CODES.ASSERTION_ERROR )
 })
})

Errores

Los errores personalizados son una característica que nos permite definir y lanzar errores específicos dentro del código.

La capacidad de definir errores personalizados es muy útil porque nos permite una mayor especificidad en la lógica de manejo de excepciones en un contrato inteligente. Los errores personalizados pueden ayudarnos a comunicar información más detallada sobre por qué ocurrió una excepción, lo que facilita la depuración y el mantenimiento del código.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;

contract HardhatMatcherTester {

 error ErrorWithArg(address hardhatMatcherTesterAddress);

 function errorWithArg() external view {
  revert ErrorWithArg(address(this));
 }
}
describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if throw BasicError when call basicError', async () => {
 await expect( hardhatMatcherTester.errorWithArg() )
  .to.be.revertedWithCustomError(hardhatMatcherTester, 'ErrorWithArg')
 })
})
describe('HardhatMatcherTester Tests', () => {
 let hardhatMatcherTester: HardhatMatcherTester

 it('Check if throw ErrorWithArg when call errorWithArg', async () => {
  await expect(hardhatMatcherTester.errorWithArg())
   .to.be.revertedWithCustomError(hardhatMatcherTester, 'ErrorWithArg')
   .withArgs(await hardhatMatcherTester.getAddress())
 })
})

Proper Address

El atributo properAddress proporciona una forma sencilla y eficiente de comprobar si una dirección es válida y cumple con ciertos criterios. Puede utilizarse para verificar si una dirección está en un formato adecuado.

describe('HardhatMatcherTester Tests', () => {
    let hardhatMatcherTester: HardhatMatcherTester

    it('Check if proper address', async () => {
        expect(await hardhatMatcherTester.getAddress()).to.be.a.properAddress
    })
})

Conclusión

Hardhat nos ofrece varios métodos para realizar nuestros tests de forma efectiva. Los distintos tipos de pruebas que se pueden realizar con Hardhat abarcan desde pruebas unitarias hasta pruebas de integración y pruebas end to end.

Podemos llevar a cabo una variedad de tests para garantizar la calidad y robustez de nuestros contratos. Ya sea para probar la funcionalidad interna, la interacción entre contratos o la experiencia del usuario final, Hardhat proporciona las herramientas necesarias para realizar pruebas exhaustivas y asegurarnos de que nuestros contratos inteligentes funcionen como se espera en un entorno de producción.