¿Cómo depurar smart contracts con Hardhat?

Existen múltiples herramientas para depurar smart contracts con Hardhat, tanto para hacerlo de forma sistemática como de forma más casual.

Las pruebas sistemáticas se pueden completar codificando casos de uso a modo de tests unitarios, al estilo de los entornos clásicos de programación. De este modo se pueden idear baterías de pruebas que validen las distintas funciones
desarrolladas en los smart contracts, alcanzando el grado de cobertura que se considere adecuado.

Son muy útiles para corregir errores de programación, asegurar los principales casos de uso de las funcionalidades desarrolladas y probar secuencias de interacción con los contratos.

Las depuraciones en un estilo más casual se pueden completar interactuando con los contratos de forma manual a través de la Hardhat Console o programando scripts para ejecutar de forma repetida en el tiempo. Este tipo de pruebas será analizado en otro artículo posterior.

Se recomienda revisar el artículo sobre desarrollar smart contracts con Hardhat para refrescar los detalles generales sobre la instalación y configuración de Hardhat

¿Cómo programar test unitarios con Hardhat?

La dinámica general de los test unitarios con Hardhat se rige por los principios de la herramienta de test Mocha.

En líneas generales cada grupo de tests se engloba dentro de un bloque «describe». Cada test dentro de ese bloque se define dentro de un bloque «it».

Para ejecutar precondiciones y postcondiciones de forma controlada al principio y al final de cada test se utilizan los «hooks» de tipo «before/beforeEach» (antes del primer test del bloque/antes de cada test del bloque) y «after/afterEach» (después del último test del bloque/después de cada test del bloque).

Las precondiciones pueden ser útiles para inicializar en un estado conocido el nodo de red en que se ejecutan los test del bloque o para establecer un estado inicial conocido al comienzo de cada test. Aunque esta es una práctica habitual ya desde los tiempos de Truffle, en el entorno Hardhat se plantea una posibilidad más óptima y eficiente, los llamados fixtures de Hardhat.

Las postcondiciones suelen utilizarse para invocar funciones de limpieza al finalizar la ejecución de los tests.

Las comprobaciones de los resultados de los test emplean en general las aserciones y funciones de la herramienta chai, si bien conviene instalar y definir como dependencia del fichero «hardhat.config.js» el plugin «hardhat-chai-matchers» para añadir capacidades específicas de Ethereum a la librería chai. Para que las funciones de esta librería funcionen correctamente es preciso configurar Hardhat en autominado de transacciones (modo por defecto), es decir cada transacción en un bloque nuevo.

Todos estos conceptos se pueden ampliar en la página oficial de Hardhat para depurar los contratos.

Los test se agrupan en suites en ficheros independientes en la carpeta definida para los test en el fichero «hardhat.config.js» (directorio «test» por defecto).

Se ejecutan de la siguiente forma:

yarn hardhat test .\test\test_suite.js => executes just the test suite on "test_suite.js" file
yarn hardat test => executes every test file

¿Qué opciones de configuración se pueden aplicar?

Las opciones de configuración más interesantes para el fichero «hardhat.config.js» desde el punto de vista de los test son las siguientes:

  • loggingEnabled (true/false): controla si el componente Hardhat Network registrará en la consola de shell información de depuración para cada petición JSON-RPC. Por defecto está deshabilitado en caso de lanzar la Hardhat Network como un nodo efímero y habilitado si se trata de un proceso de larga duración.
  • throwOnTransactionFailures (true/false): controla si el componente Hardhart Network lanzará una excepción en caso de error en una transacción de escritura en la blockchain (modo por defecto) para facilitar la comprobación de los test. De esa forma se pueden usar las funciones «to.be.reverted» propias de la librería «hardhat-chai-matchers» para asegurar que se han lanzado los errores oportunos durante la ejecución de la transacción.
  • throwOnCallFailures (true/false): controla si el componente Hardhart Network lanzará una excepción en caso de error en una lectura (call) sobre la blockchain (modo por defecto) para facilitar la comprobación de los test. Comparte utilidad con el caso anterior.

Si bien los valores por defecto para las opciones presentadas son en general los más adecuados, se adjunta un ejemplo de fichero «hardhat.config.js» ilustrando la definición explícita. Se observa que las opciones aplican a la red identificada como «hardhat» en la sección «networks»:

require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-chai-matchers");

module.exports = { 
	networks: {
      hardhat: {
        loggingEnabled: false,
        throwOnTransactionFailures: true,
        throwOnCallFailures: true
      },
	},	  
    solidity: {
      version: "0.8.23"
    },       
    paths: {
      sources: "./contracts",
      tests: "./test",
      cache: "./cache",
      artifacts: "./artifacts"
    }
}

¿Qué otras opciones de log existen para depurar?

Además de la mencionada opción de configuración «loggingEnabled» propia del fichero «hardhat.config.js» existen otras posibilidades:

  • Ejecutar los test con el modificador «–verbose». Se mostrará mucha más información de depuración por la consola de shell.
yarn hardat test --verbose
  • Añadir llamadas a «console.log» desde el código de los smart contracts en Solidity. Actúa como la función homónima de Node.js. La salida aparecerá mezclada con la propia de la ejecución de los test. Es preciso importar el contrato «hardhat/console.sol». Se pueden conocer los detalle completos en la documentación de referencia de Hardhat. Un ejemplo directamente extraído de la página oficial de Hardhat sería el siguiente:
import "hardhat/console.sol";

function transfer(address to, uint256 amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");

    console.log(
        "Transferring from %s to %s %s tokens",
        msg.sender,
        to,
        amount
    );

    balances[msg.sender] -= amount;
    balances[to] += amount;

    emit Transfer(msg.sender, to, amount);
}

Siguientes pasos: ejecución de scripts

Hardhat proporciona múltiples recursos para depurar los smart contracts tanto de forma sistemática como más manual. 

En caso de optar por el desarrollo de baterías de pruebas a partir de test unitarios, el modo de programación es muy parecido a otros entornos de desarrollo clásicos, incluyendo la ahora obsoleta herramienta Truffle, usada desde los incios de la programación de smart contracts.

En este artículo se han cubierto los fundamentos de la programación de test unitarios con Hardhat así como algunas opciones útiles de configuración. 

En próximos artículos se abordarán otros ciclos del proceso de pruebas como son la ejecución de scripts y el uso de la consola para depurar transacciones.


¿Sueles programar tests sistemáticos en tus smart contracts? ¿Usas alguna otra herramienta? Comparte tus experiencias enviando comentarios.

Si necesitas un desarrollador de smart contracts, contáctame: mi método de trabajo siempre pasa por cubrir con garantías la funcionalidad programada mediante pruebas automatizadas. ¡Muchas gracias!