Desarrolladores

La Guía Definitiva para Probar Contratos Inteligentes en Stellar

Autor

Leigh McCulloch

Fecha de publicación

¿Por qué Probar?

¿Cómo sabemos que algo funciona?

Los científicos han utilizado el método científico durante siglos. Wikipedia describe el método científico como:

“Un método para adquirir conocimiento que implica cuidadosa observación unida a riguroso escepticismo, porque las suposiciones cognitivas pueden distorsionar la interpretación de la observación.

– Wikipedia “método científico” (CC BY-SA 4.0)

Esto realmente es una forma de decir que si no somos rigurosamente escépticos sobre si algo funciona, estamos haciendo suposiciones. Hay muchas cosas dichas sobre las suposiciones:

  • Las suposiciones hacen un burro de ti y de mí.
  • Las suposiciones son la madre de todos los errores.

Las suposiciones no probadas se convierten en errores.

Dijkstra destaca que la creación de errores es una parte natural del desarrollo:

“Si depurar es el proceso de eliminar errores de software, entonces programar debe ser el proceso de introducirlos.”

– Edsger W. Dijkstra

Por lo tanto, para que las pruebas sean efectivas al identificar errores tempranamente, deben integrarse con el desarrollo, justo como la creación de errores. No un pensamiento posterior, o agregado después.

Hay un límite en nuestra capacidad como humanos para crear cosas que funcionen perfectamente. Por eso, gran parte de la vida—los edificios en los que vivimos y los autos que conducimos—deben ser observados y probados.

Para el software, es muy similar. Como ingenieros de software, constructores de blockchain y desarrolladores de contratos, usamos el método científico, la observación cuidadosa y el escepticismo riguroso.

Nosotros probamos.

Las pruebas automatizadas son una de las herramientas más poderosas para crear software más confiable y seguro.

Buenas pruebas:

  • Confirman que el software hace lo que se pretendía la primera vez que se construyó;
  • Proporcionan confianza para refactorizar el código sabiendo que la funcionalidad no ha cambiado;
  • Ayudan a los desarrolladores a entender y probar el alcance de un cambio;
  • Son un multiplicador en los equipos porque dan a otros la confianza para cambiar código con el que no tienen experiencia previa.

Buenas herramientas y estrategias de prueba hacen nuestro escepticismo mucho más riguroso de lo que solos somos capaces.

¿Cómo es Diferente Probar Contratos Stellar?

Stellar es uno de los blockchains OG, en línea desde 2014, y lanzó contratos inteligentes a Mainnet en 2024.

Antes de lanzar contratos inteligentes, Stellar ya tenía una gran experiencia en pruebas, incluyendo:

  • Una red de prueba muy estable y accesible;
  • API y RPCs alojados públicamente y gratuitos en la red de prueba;
  • Un friendbot confiable para obtener lumens de prueba;
  • El Lab (https://lab.stellar.org) para inspeccionar y construir transacciones, y explorar APIs;
  • Una imagen docker ligera para ejecutar una red real local y desplegable completa con APIs y un bot de lumen de prueba, y cierres de libro mayor acelerados— ideal para pruebas fáciles y rápidas localmente y en CI.

Cuando Stellar agregó contratos inteligentes, el enfoque fue nuevamente en una gran experiencia de prueba que incluyó una caja de herramientas rica que llevó a los desarrolladores a escribir pruebas tan pronto como su primer contrato. Probar contratos inteligentes en Stellar incluye:

  • Las pruebas son sin problemas con un solo lenguaje—Rust—y una cadena de herramientas unificada. Los desarrolladores escriben contratos en Rust y los prueban usando las mismas APIs, habilitando pruebas unitarias, pruebas de integración contra contratos de Mainnet, y pruebas con datos de producción real. También pueden aprovechar pruebas de fuzz y de propiedad—todo en Rust, todo con las mismas APIs—para una experiencia consistente.
  • Las pruebas están potenciadas por el mismo tiempo de ejecución de contratos Soroban que se ejecuta en Mainnet. No hay emuladores ni marcos de prueba tratando de ponerse al día con la realidad.
  • Reduce el cambio de contexto sin marcos adicionales para aprender.
  • Un ecosistema rico de herramientas de prueba que ya existe. Usa las herramientas de Rust que ya conoces y te encantan.
  • Las pruebas de Stellar están respaldadas por un ecosistema robusto de IDE herramientas que ya existen, con servidores de lenguaje y depuración paso a paso, para que puedas desarrollar con el IDE que ya conoces y con una experiencia inmersiva.
  • Ya sea VSCode, Cursor, RustRover, Sublime, Vim, Zed, Windsurf, etc. Cualquier cosa que soporte el servidor de lenguaje Rust rust-analyzer va a ayudar a escribir pruebas, y cualquier cosa que soporte LLDB te ayudará a depurar tu código paso a paso.

¿Qué es probar?

A menudo se piensa en las pruebas como pruebas unitarias, pero las pruebas son mucho más. En el ecosistema de Stellar, escribir pruebas para contratos inteligentes implica, al menos, todas las siguientes estrategias.

Cómo Escribir Pruebas

Pruebas Unitarias

Las pruebas unitarias son pruebas pequeñas que prueban una pieza de funcionalidad dentro de un contrato. Son un gran lugar para comenzar y en muchos ecosistemas, son la prueba más simple de escribir. Las pruebas unitarias son útiles cuando el código a probar es una parte estrecha del programa sin pocas o ninguna dependencia.

A continuación se muestra un ejemplo de una prueba unitaria que prueba la funcionalidad de un contrato de incremento.

#![cfg(test)]
use soroban_sdk::Env;
use crate::{IncrementContract, IncrementContractClient};

#[test]
fn test() {
   let env = Env::default();
   let contract_id = env.register(IncrementContract, ());
   let client = IncrementContractClient::new(&env, &contract_id);

   assert_eq!(client.increment(), 1);
   assert_eq!(client.increment(), 2);
   assert_eq!(client.increment(), 3);
}

Para saber más sobre el ejemplo anterior y escribir pruebas unitarias, consulta la guía práctica sobre Pruebas Unitarias.

Cómo Escribir Pruebas

Pruebas de Integración

Las pruebas de integración son pruebas que abarcan múltiples componentes e incluyen los puntos de integración entre ellos, por lo que naturalmente prueban un alcance mayor. Una prueba de integración para un contrato probablemente pruebe otros contratos de los que el contrato depende.

El SDK de Soroban Rust facilita las pruebas de integración al proporcionar utilidades para probar contra contratos reales obtenidos de Mainnet, Testnet o el sistema de archivos local. Debido a que todas las pruebas usan el entorno real de Soroban no hay diferencia en las herramientas o la configuración de la prueba para escribir pruebas unitarias o de integración.

Para realizar pruebas de integración contra contratos desplegados en Mainnet o Testnet, usa el stellar-cli para obtener el contrato y luego importarlo en la prueba.

$ stellar contract fetch --id C... --out-file pause.wasm

A continuación se muestra un ejemplo de una prueba de integración que prueba la funcionalidad de un contrato de incremento junto con una dependencia, el contrato de pausa que se descargó de Testnet.

#![cfg(test)]
use soroban_sdk::Env;
use crate::{Error, IncrementContract, IncrementContractClient};

mod pause {
   soroban_sdk::contractimport!(file = "pause.wasm");
}

#[test]
fn test() {
   let env = Env::default();

   let pause_id = env.register(pause::WASM, ());
   let pause_client = pause::Client::new(&env, &pause_id);

   let contract_id = env.register(
       IncrementContract,
       IncrementContractArgs::__constructor(&pause_id),
   );
   let client = IncrementContractClient::new(&env, &contract_id);

   pause_client.set(&false);
   assert_eq!(client.increment(), 1);

   pause_client.set(&true);
   assert_eq!(client.try_increment(), Err(Ok(Error::Paused)));

   pause_client.set(&false);
   assert_eq!(client.increment(), 2);
}

Para saber más sobre el ejemplo anterior y escribir pruebas de integración, consulta la guía práctica sobre Pruebas de Integración.

Cómo Escribir Pruebas

Pruebas de Bifurcación

Las pruebas de integración que prueban con contratos de Mainnet también pueden incorporar datos de Mainnet para que la prueba de integración sea lo más similar posible a una transacción enviada a Mainnet.

Para realizar pruebas de integración contra contratos y sus datos de Mainnet o Testnet, usa el stellar-cli para tomar una instantánea del contrato y sus datos, luego carga esa instantánea en la prueba.

$ stellar snapshot create --address C... --output json --out snapshot.json

A continuación se muestra un ejemplo de una prueba de integración que prueba la funcionalidad de un contrato de incremento junto con una dependencia, el contrato de pausa que se capturó junto con sus datos y se cargó en el entorno en la prueba.

use soroban_sdk::Env;
use crate::{IncrementContract, IncrementContractClient};

#[test]
fn test() {
   let env = Env::from_ledger_snapshot_file("snapshot.json");

   let pause_id = Address::from_str(&env, "C...");
   let contract_id = env.register(
       IncrementContract,
       IncrementContractArgs::__constructor(&pause_id),
   );
   let client = IncrementContractClient::new(&env, &contract_id);

   assert_eq!(client.increment(), 1);
}

Cómo Escribir Pruebas

Fuzzing

El fuzzing es el proceso de proporcionar datos aleatorios al programa para identificar comportamientos inesperados, como fallos y pánicos. Las pruebas de fuzz también pueden escribirse como pruebas de propiedad que van un paso más allá y afirman que alguna propiedad permanece verdadera independientemente de la entrada. Por ejemplo, un contrato de token puede tener una propiedad de que todos los saldos nunca se vuelvan negativos, y una prueba de propiedad puede usar un conjunto de herramientas de fuzzing para probar una gran variedad de entradas para probar que permanece verdadera.

A continuación se muestra un ejemplo de una prueba de fuzz que prueba que las llamadas repetidas a un contrato de incremento devuelven un valor que es mayor que el último, y los únicos errores vistos son errores definidos por el contrato.

#![no_main]
use libfuzzer_sys::fuzz_target;
use soroban_increment_with_fuzz_contract::{IncrementContract, IncrementContractClient};
use soroban_sdk::{
   testutils::arbitrary::{arbitrary, Arbitrary},
   Env,
};

#[derive(Debug, Arbitrary)]
pub struct Input {
   pub by: u64,
}

fuzz_target!(|input: Input| {
   let env = Env::default();
   let id = env.register(IncrementContract, ());
   let client = IncrementContractClient::new(&env, &id);

   let mut last: Option<u32> = None;
   for _ in input.by.. {
       match client.try_increment() {
           Ok(Ok(current)) => assert!(Some(current) > last),
           Err(Ok(_)) => {} // Expected error
           Ok(Err(_)) => panic!("success with wrong type returned"),
           Err(Err(_)) => panic!("unrecognised error"),
       }
   }
});

Para saber más sobre el ejemplo y escribir pruebas de fuzz y pruebas de propiedad, consulta la guía práctica sobre Fuzzing.

Cómo Escribir Pruebas

Pruebas Diferenciales

Las pruebas diferenciales son las pruebas de dos cosas para descubrir diferencias en su comportamiento. El objetivo es demostrar que las dos cosas se comportan de manera consistente y que no divergen en comportamiento.

Esta estrategia es efectiva al construir algo nuevo que debería comportarse como algo que ya existe. Eso podría ser una nueva versión de un contrato, o podría ser el mismo contrato construido con una nueva versión de un SDK u otra dependencia, o podría ser una refactorización que no espera cambios funcionales.

Cuando realices pruebas diferenciales contra un contrato desplegado, usa el stellar-cli para obtener el contrato de Mainnet o Testnet.

$ stellar contract fetch --id C... –-out-file pause.wasm

A continuación se muestra un ejemplo de una prueba diferencial que prueba que una versión desplegada de un contrato y una nueva versión del contrato se comportan igual y emiten los mismos eventos.

#![cfg(test)]
use crate::{IncrementContract, IncrementContractClient};
use soroban_sdk::{testutils::Events as _, Env};

mod deployed {
   soroban_sdk::contractimport!(file = "contract.wasm");
}

#[test]
fn differential_test() {
   let env = Env::default();
   assert_eq!(
       // Baseline – the deployed contract
       {
           let contract_id = env.register(deployed::WASM, ());
           let client = IncrementContractClient::new(&env, &contract_id);
           (
               // Return Values
               (
                   client.increment(),
                   client.increment(),
                   client.increment(),
               ),
               // Events
               env.events.all(),
           )
       },
       // Local – the changed or refactored contract
       {
           let contract_id = env.register(IncrementContract, ());
           let client = IncrementContractClient::new(&env, &contract_id);
           (
               // Return Values
               (
                   client.increment(),
                   client.increment(),
                   client.increment(),
               ),
               // Events
               env.events.all(),
           )
       },
   );
}

Para saber más sobre cómo escribir pruebas diferenciales, consulta la guía práctica sobre Pruebas Diferenciales.

Cómo Escribir Pruebas

Pruebas Diferenciales con Instantáneas de Prueba

Todos los contratos construidos con el SDK de Soroban Rust tienen una forma de prueba diferencial incorporada y habilitada por defecto. El SDK de Soroban Rust genera una instantánea de prueba al final de cada prueba que involucra el Entorno de Soroban. La instantánea se escribe en un archivo JSON en el directorio test_snapshots.

Compromete las instantáneas al control de versiones y en cambios futuros las instantáneas de prueba cambiarán si el resultado cambió. Si la instantánea de prueba cambia en momentos que no esperas, como actualizar un SDK o refactorizar, puede ser una señal de que la funcionalidad observable del contrato también ha cambiado.

#![cfg(test)]
use soroban_sdk::Env;

use crate::{Contract, ContractClient};

#[test]
fn test_abc() {
   let env = Env::default();
   let contract_id = env.register_contract(None, Contract);
   let client = ContractClient::new(&env, &contract_id);

   assert_eq!(client.increment(), 1);

   // At the end of the test the Env will automatically write a test snapshot
   // to the following directory: test_snapshots/test_abc.1.json
}

Para saber más sobre cómo usar instantáneas de prueba, consulta la guía práctica sobre Pruebas Diferenciales con Instantáneas de Prueba.

Cómo Escribir Pruebas

Pruebas de Mutación y Cobertura de Código

Medir la cobertura del código utiliza herramientas para identificar líneas de código que son y no son ejecutadas por pruebas. Las estadísticas de cobertura de código pueden darnos una idea de cuánto de un contrato es realmente probado por sus pruebas.

Una forma de identificar código no probado es usar herramientas de cobertura de código para contar e identificar líneas de código que no son ejecutadas por pruebas.

En pruebas unitarias e integración, la herramienta Rust cargo-llvm-cov informará estadísticas de cobertura de código e identificará líneas. Ver Cobertura de Código.

En pruebas de fuzzing, la herramienta Rust cargo-fuzz contiene funcionalidad de cobertura. Ver Fuzzing.

Otra forma de identificar código no probado es con pruebas de mutación. Las pruebas de mutación hacen cambios en un programa para identificar cambios que se pueden hacer que no son detectados por las pruebas.

La herramienta Rust cargo-mutants puede ser usada para pruebas de mutación de contratos. Ver Pruebas de Mutación.

Conclusión

Escribir pruebas en contratos Stellar está completamente integrado en el SDK Rust de Soroban y viene con el verdadero Entorno Soroban, así que el contrato no está siendo probado contra un emulador o simulador, es el trato real.

Refuerza las pruebas con las siguientes guías prácticas: