NUEVO: Helius adquiere Light Protocol
Guía para probar programas de Solana
Blog/Desarrollo

Guía para probar programas de Solana

Developer Experience Engineer0xIchigo en X0xIchigo en LinkedIn0xIchigo en GitHub
27 min de lectura

Introducción

Las pruebas en un entorno blockchain trascienden el paradigma tradicional de pruebas de software e introducen desafíos únicos y consecuencias más graves. En el entorno de alto rendimiento y baja latencia de Solana, el margen de error es reducido. Las pruebas automatizadas no son solo una práctica recomendada, sino una necesidad fundamental para garantizar la confiabilidad y seguridad de los programas que operan en el entorno dinámico e implacable de Solana.

Este artículo explora los principales tipos de pruebas automatizadas: pruebas unitarias, pruebas de integración y pruebas de extremo a extremo (E2E). También explica cómo escribir pruebas unitarias básicas en JavaScript/TypeScript y Rust antes de analizar frameworks populares para probar Solana. El artículo termina con un ejemplo práctico que prueba un programa de juego llamado “King of the Hill”.

Si Solana es algo nuevo para ti, te recomiendo leer primero estas publicaciones anteriores del blog:

Este artículo también complementa nuestro artículo anterior sobre la seguridad de los programas de Solana. Te recomiendo leer ambos en conjunto.

¿Qué son las pruebas?

Las pruebas se realizan para verificar que un fragmento de código o una aplicación completa funcionen según lo previsto. Existen dos tipos generales de pruebas:

  • Pruebas manuales: un proceso centrado en las personas, en el que los desarrolladores, analistas de control de calidad, especialistas en pruebas de penetración u otros responsables ejecutan casos de prueba
  • Pruebas automatizadas: un proceso centrado en el código, en el que se escriben scripts para ejecutar mediante programación casos de prueba predefinidos

Las pruebas manuales son un proceso muy flexible e independiente del tipo de aplicación que se prueba. Son adecuadas para probar nuevas funciones, usabilidad y accesibilidad. Dependen de la intuición de quien realiza la prueba sobre cómo debería comportarse una aplicación. Sin embargo, son inherentemente lentas, propensas a errores, requieren mucho tiempo y suelen estar incompletas —es decir, no cubren todos los escenarios— debido a la falta de herramientas en el proceso de prueba. 

Las pruebas automatizadas buscan resolver las desventajas de las pruebas manuales. Por ejemplo, suelen ser más rápidas, especialmente si se ejecutan en paralelo. Son menos propensas a errores humanos porque siguen un script predefinido. Además, aumentan la cobertura gracias a su capacidad para gestionar con eficiencia una gran cantidad de casos de prueba, lo que las convierte en una solución muy escalable. Sin embargo, debido a su objetividad rígida, son menos precisas para pruebas que dependen de la interacción humana, el juicio o el razonamiento crítico.

Para este artículo, nos enfocaremos en las pruebas automatizadas de nuestros programas de Solana, ya que probarlos manualmente en mainnet sería bastante costoso y hacerlo en devnet requeriría mucho tiempo. Sin embargo, tanto las pruebas manuales como las automatizadas deben formar parte del proceso antes de enviar código a producción. Un proceso de pruebas sólido puede minimizar la cantidad de errores que llegan a producción al identificarlos antes durante el desarrollo.

Existen varios tipos de pruebas automatizadas:

  • Pruebas unitarias
  • Pruebas de integración
  • Pruebas de extremo a extremo (E2E)

Pruebas unitarias

Las pruebas unitarias son un proceso en el que se prueban las unidades funcionales más pequeñas del código para garantizar que funcionen correctamente. Idealmente, las unidades son los bloques más pequeños posibles de un programa —por ejemplo, funciones o módulos individuales— que, al combinarse, forman el producto terminado. La idea central es que el programa completo debería funcionar según lo previsto si probamos exhaustivamente sus componentes básicos. 

Las pruebas unitarias son fundamentales para el desarrollo en Solana porque garantizan que cada parte de un programa se comporte según lo previsto. La reutilización de estas pruebas permite verificar que las nuevas funciones o actualizaciones cumplan con las especificaciones del proyecto y las expectativas de los usuarios definidas en el caso de prueba. Por eso, las pruebas unitarias fomentan de forma inherente la optimización y refactorización del código para que las nuevas mejoras no afecten negativamente la funcionalidad del programa. No solo garantizan que cada segmento de código funcione correctamente bajo distintas condiciones de prueba, sino también que las interacciones con la blockchain sean eficientes y seguras. Detectar errores temprano mediante pruebas unitarias es crucial, ya que evita que posibles vulnerabilidades lleguen a producción. 

Varios frameworks de pruebas ayudan a agilizar las pruebas unitarias al simplificar la simulación de condiciones de red y la administración del estado del programa, como veremos más adelante. Con las pruebas unitarias, los desarrolladores de Solana pueden alcanzar un alto nivel de confiabilidad y rendimiento del código.

Pruebas de integración

Las pruebas de integración van más allá de las pruebas unitarias para examinar cómo funcionan juntas las distintas unidades de un programa. Verificar que las funciones y los módulos de un programa operen en conjunto es crucial para el desarrollo en Solana, donde las interacciones entre programas suelen ser complejas y tener consecuencias financieras. Las pruebas de integración buscan identificar y resolver problemas que no son evidentes cuando las unidades se prueban por separado, pero surgen cuando los componentes interactúan. Estos problemas pueden incluir incompatibilidades en formatos de datos, inconsistencias de tipos, dependencias entre programas o inconvenientes con API de terceros.

En el contexto de Solana, donde los programas interactúan de forma inherente con otros programas, billeteras y oráculos, las pruebas de integración verifican que estas interacciones ocurran según lo previsto. Aunque cada unidad funcione perfectamente, su combinación podría introducir comportamientos inesperados o ineficiencias que surgen bajo estas condiciones simuladas. Los desarrolladores pueden usar distintos frameworks de pruebas para simular diferentes flujos de transacciones e interacciones entre programas, reproduciendo de cerca situaciones reales. Por ejemplo, Bankrun es un framework de pruebas sólido y ligero que permite a los desarrolladores avanzar y retroceder en el tiempo, además de establecer dinámicamente los datos de las cuentas. Esto no es posible al usar solana-test-validator. Las pruebas de integración son cruciales para garantizar que un programa sea sólido, confiable y esté preparado para las exigencias de las condiciones de red de Solana.

Pruebas de extremo a extremo (E2E)

Las pruebas de extremo a extremo (E2E) son la culminación del proceso de pruebas. Se enfocan en evaluar el flujo operativo completo de un programa tal como ocurriría en situaciones reales. Esta metodología difiere de las pruebas unitarias y de integración porque examina el programa desde la perspectiva del usuario: todos los flujos y funciones posibles que encontraría un usuario final deben operar según lo previsto. 

Las pruebas E2E son esenciales para verificar que un programa cumpla con sus requisitos funcionales y ofrezca una experiencia de usuario fluida. Esta fase ayuda a descubrir problemas que podrían no haber sido evidentes durante las pruebas unitarias o de integración, como retrasos en el procesamiento de transacciones, problemas para conservar el estado, optimizaciones de unidades de cómputo o condiciones de red inesperadas. Aunque las pruebas E2E suelen aplicarse a una dApp completa, probar los flujos operativos de un programa y verificar cómo interactuaría la transacción de un usuario con sus distintas funciones y módulos es crucial para crear un programa exitoso y seguro.

Combinación de estas metodologías de pruebas

Es crucial aplicar una estrategia de pruebas por capas y combinar pruebas unitarias, de integración y E2E en tu proceso de desarrollo. Cada metodología cumple una función distinta en el ciclo de desarrollo y aborda diferentes aspectos de la funcionalidad y el rendimiento de un programa.

Las pruebas unitarias son la base de un enfoque por capas. Permiten a los desarrolladores identificar y resolver rápidamente los problemas en el nivel más granular del código. Aunque son excelentes para garantizar la corrección objetiva de funciones o módulos individuales, no consideran cómo funcionan juntas estas unidades ni cómo se integran en la experiencia del usuario.

Las pruebas de integración cubren esta brecha al evaluar cómo interactúan las distintas unidades para descubrir problemas que surgen al integrar estos componentes. Sin embargo, por sí solas, es posible que no representen por completo la experiencia del usuario final ni el comportamiento del programa en condiciones reales.

Las pruebas E2E complementan las pruebas unitarias y de integración al simular situaciones reales de usuario y probar las aplicaciones como un todo. Este enfoque es muy valioso para evaluar la experiencia general del usuario, pero no proporciona la información granular necesaria para identificar y resolver rápidamente problemas específicos.

Al integrar estas metodologías, los desarrolladores pueden crear un framework de pruebas sólido que cubra todo el espectro de posibles problemas. Un enfoque integral no solo mejora la calidad y seguridad del programa, sino que también agiliza el proceso de desarrollo. Los desarrolladores pueden tomar decisiones fundamentadas y realizar revisiones rápidamente, con la confianza de que sus cambios se evaluarán en varios niveles. Combinar estas metodologías es crucial para garantizar que un programa de Solana sea técnicamente sólido y cumpla con las expectativas de los usuarios en condiciones reales antes del despliegue.

Cómo escribir buenas pruebas

Escribir pruebas eficaces es vital para desarrollar programas de Solana confiables y seguros. La esencia de una buena prueba está en enfocarse en el comportamiento que se evalúa, no en el framework utilizado ni en los detalles de implementación del código. Los desarrolladores pueden crear una estrategia de pruebas eficiente que mejore la calidad del código al integrar los principios del desarrollo guiado por pruebas (TDD), el patrón Preparar-Actuar-Afirmar (AAA) y las prácticas recomendadas de la industria.

Desarrollo guiado por pruebas (TDD)

El TDD es un enfoque sólido de desarrollo de software guiado por la escritura de pruebas. Es decir, la idea principal es escribir las pruebas antes que el código real. El ciclo de TDD suele incluir tres pasos:

  • Escribir una prueba que falle: el desarrollo debe comenzar con una prueba para la siguiente funcionalidad que se quiera agregar. La prueba fallará inevitablemente porque la funcionalidad evaluada todavía no existe
  • Implementar el código: los desarrolladores deben escribir la cantidad mínima de código necesaria para que la prueba pase. El objetivo es la rapidez y la simplicidad
  • Refactorizar: una vez que la prueba pase, los desarrolladores deben refactorizar el código para mejorar su estructura y claridad sin cambiar su comportamiento. La prueba exitosa actúa como una red de seguridad contra cambios incompatibles. Este paso puede incluir eliminar código duplicado, dividir métodos en unidades más pequeñas, reorganizar jerarquías de herencia o usar nombres descriptivos. En el contexto de Solana, podría implicar optimizar la cantidad de CU solicitadas para una transacción, reducir la cantidad de CPI o agilizar los flujos de transacciones.

Aunque el TDD no es necesario para crear buenos programas de Solana, los desarrolladores deberían considerar su filosofía: promueve un enfoque meticuloso para crear programas, su naturaleza iterativa fomenta un proceso de desarrollo flexible y adaptable, y se ajusta perfectamente a la necesidad de precisión, seguridad y eficiencia. El TDD anima a los desarrolladores a escribir código más limpio y enfocado, optimizado para los requisitos únicos de red y rendimiento de Solana —por ejemplo, optimizar las CU—. 

El patrón Preparar-Actuar-Afirmar (AAA)

El patrón AAA ofrece una estructura simple pero potente para escribir pruebas claras, concisas y eficaces. En esencia, fomenta un enfoque disciplinado para escribir pruebas dividido en tres fases distintas:

  • Preparar: comienza por configurar el entorno de prueba y preparar las entradas relevantes. Esto puede implicar generar cuentas, simular saldos o preparar instrucciones. El objetivo es crear una situación controlada que reproduzca las condiciones en las que se probará el comportamiento
  • Actuar: ejecuta el comportamiento que se está probando. El enfoque está en la acción que desencadena el comportamiento que quieres evaluar. Por ejemplo, ¿qué ocurre cuando llamo a la función x y le paso la cuenta y?
  • Afirmar: evalúa el resultado de la acción frente al resultado esperado. Este paso es crucial para verificar si la prueba pasa o falla. Por ejemplo, las afirmaciones pueden ir desde una comprobación sencilla de valores hasta validaciones complejas que involucren varios cambios de estado. La implementación de estas afirmaciones depende, en última instancia, del framework o protocolo utilizado. Lighthouse es un programa que proporciona instrucciones de afirmación que pueden añadirse a las transacciones para identificar estados no deseados, resultados de simulación falsificados o gastos excesivos, entre otros casos. Exploraremos con mayor detalle los beneficios y aspectos complejos de Lighthouse en otro artículo.

La fortaleza del patrón AAA está en su adaptabilidad, lo que lo hace útil para pruebas unitarias, de integración y E2E. Por ejemplo:

  • Pruebas unitarias: una prueba de una función determinada podría preparar el estado del programa, actuar invocando la función y afirmar comprobando el valor devuelto o los cambios de estado resultantes
  • Pruebas de integración: probar las interacciones entre varios programas puede implicar preparar el despliegue de los programas y establecer sus estados iniciales, actuar ejecutando las transacciones relevantes y afirmar verificando el estado final de cada programa involucrado
  • Pruebas E2E: una prueba E2E de un programa podría preparar el estado del programa, actuar recorriendo un flujo de usuario esperado completo —por ejemplo, crear una cuenta, crear una propuesta, votar por ella, finalizar su fase de votación, etc.— y afirmar comparando los resultados del flujo con los resultados esperados

El patrón AAA es crucial para el desarrollo de programas. Impone un enfoque de pruebas centrado en el comportamiento, necesario para comprobar si un programa actuará según lo previsto. Las pruebas estructuradas en torno a AAA son más fáciles de entender y mantener porque cada fase se divide claramente en pasos de configuración, acción y verificación. Además, AAA promueve la creación de pruebas independientes y desacopladas que se enfocan en comportamientos o interacciones específicos.

Prácticas recomendadas de la industria

Escribir buenas pruebas no es algo exclusivo del desarrollo en Solana. Podemos aplicar al proceso de probar programas de Solana los conocimientos generales del desarrollo de software y enfocarnos en evaluar los comportamientos previstos, sin quedar atrapados en los detalles de implementación. 

Por ejemplo, las pruebas unitarias deberían enfocarse, por lo general, en la interfaz pública de un método, proporcionar argumentos específicos y verificar que los resultados sean los esperados. Este enfoque garantiza que las pruebas unitarias sigan siendo válidas aunque cambie la implementación interna del método, siempre que el comportamiento se mantenga. En el contexto del desarrollo en Solana, esto significa que los cambios en la lógica de un programa que no afecten su comportamiento externo no deberían requerir ninguna refactorización de las pruebas.

Además, un error común al escribir pruebas unitarias es hacer que dependan demasiado del funcionamiento interno del método evaluado. Esto incluye esperar que ciertos métodos privados se llamen una cantidad específica de veces o estén programados de una forma determinada. Estas pruebas son demasiado frágiles y propensas a fallar ante cualquier refactorización del código, incluso cuando el comportamiento real del método no cambia. En su lugar, debes enfocarte en los resultados y efectos secundarios del método que puedan observarse externamente. Aquí se pueden usar herramientas de cobertura de código para garantizar que las pruebas sean exhaustivas sin depender demasiado de mecanismos internos.

Adoptar estas prácticas recomendadas en el desarrollo en Solana mejora la solidez y adaptabilidad de los programas. Enfocarse en probar comportamientos en lugar de detalles de implementación permite crear código más resistente y fácil de mantener, algo esencial al desplegar código en un entorno de red dinámico como Solana. Este enfoque garantiza que los cambios en la lógica del programa no requieran repetir una gran cantidad de pruebas y que el programa esté preparado para el despliegue.

Ahora comencemos a escribir algunas pruebas.

Cómo escribir pruebas unitarias básicas

Pruebas unitarias en Rust

Rust adopta un enfoque único para las pruebas unitarias y anima a los desarrolladores a colocar las pruebas en los mismos archivos que el código. Esto se hace mediante el módulo tests y se controla con el atributo #[cfg(test)]. El atributo de prueba garantiza que estas pruebas solo se compilen y ejecuten al probar explícitamente el software con el comando cargo test —es decir, no se ejecutan con el comando cargo build—. Los desarrolladores también pueden excluir pruebas de la ejecución habitual mediante el atributo #[ignore]. Esto resulta útil para pruebas particularmente lentas y permite ejecutarlas cuando se llaman explícitamente con el comando cargo test -- --ignored.

Como ejemplo, considera la siguiente función de Rust:

Código
pub fn bubble_sort<T: Ord>(array: &mut [T]) {
    if array.is_empty() {
        return;
    }

    for i in 0..array.len() {
        for j in 0..array.len() - 1 - i {
            if array[j] > array[j + 1] {
                array.swap(j, j + 1);
            }
        }
    }
}

El ordenamiento de burbuja es un tipo de algoritmo de ordenamiento que recorre repetidamente los elementos de una lista, compara el elemento actual con el siguiente e intercambia sus valores cuando es necesario. Si quisiéramos probar esta función para garantizar que opere según lo previsto, podríamos escribir las siguientes pruebas:

Código
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bubble_sort() {

        let mut test1 = vec![12, 39, 4, 36, 777];
        assert_eq!(bubble_sort(&mut test1), vec![4, 12, 36, 39, 777]);

        let mut test2 = vec![21, 55, 14, -123, 32, 0];
        assert_eq!(bubble_sort(&mut test2), vec![-123, 0, 14, 21, 32, 55]);

        let mut test3 = vec!["Orange", "Pear", "Apple", "Grape", "Banana"];
        assert_eq!(bubble_sort(&mut test3), vec!["Apple", "Banana", "Grape", "Orange", "Pear"]);
    }
}

Este ejemplo muestra cómo nuestro módulo tests está anotado con el atributo #[cfg(test)]. Dentro del módulo, importamos todos los elementos públicos del módulo superior al ámbito del módulo de prueba actual con use super::*;. Luego tenemos varios casos de prueba en los que afirmamos cómo deberían verse los vectores ordenados. Rust proporciona varias macros para afirmaciones, como assert! para comprobar valores verdaderos en general, assert_eq! para comprobar igualdad e assert_ne! para comprobar desigualdad. Estas afirmaciones son la base de la estrategia de pruebas de Rust: son todo lo que realmente necesitas para comenzar a escribir pruebas. 

Con un ejemplo muy básico, imagina que tienes una función que determina si una cuenta tiene saldo suficiente para pagar una transacción determinada:

Código
pub fn has_sufficient_balance(account_balance: u64, transaction_fee: u64) -> bool {
    account_balance >= transaction_fee
}

Esta función recibe dos argumentos: el saldo actual de una cuenta y la comisión esperada de la transacción. Devuelve true si el saldo de la cuenta es suficiente para cubrir la comisión de la transacción o false en caso contrario. Esto podría probarse fácilmente con la siguiente prueba unitaria:

Código
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sufficient_funds_for_transaction() {
        let account_balance = 1_000_000;
        let transaction_fee = 5_000;

        assert!(has_sufficient_balance(account_balance, transaction_fee));
    }

    #[test]
    fn insufficient_funds_for_transaction() {
        let account_balance = 1_000;
        let transaction_fee = 5_000;

        assert!(!has_sufficient_balance(account_balance, transaction_fee));
    }
}

En la primera prueba, afirmamos que has_sufficient_balance devuelve true cuando el saldo de la cuenta es considerablemente mayor que la comisión de la transacción, lo que indica que hay fondos suficientes para cubrirla. En la segunda prueba, afirmamos que has_sufficient_funds devuelve false cuando el saldo es menor que la comisión, lo que indica que no hay fondos suficientes para cubrir la transacción. 

Otras consideraciones importantes para las pruebas en Rust

Rust tiene el atributo #[should_panic] para marcar las pruebas que se espera que entren en pánico bajo ciertas condiciones. Esto resulta útil para probar rutas de gestión de errores y especificar los mensajes de pánico esperados:

Código
#[test]
#[should_panic(expected = "Divide-by-zero error")]
fn test_divide_by_zero() {
    divide_non_zero_result(0, 0);
}

A diferencia de muchos otros lenguajes, Rust permite probar directamente las funciones privadas. Esto permite realizar pruebas unitarias más detalladas, ya que cada aspecto de la funcionalidad del código puede cubrirse con ellas.

Rust también admite técnicas más avanzadas para organizar pruebas:

  • Módulos anidados: en proyectos complejos, las pruebas pueden organizarse en módulos anidados, lo que permite crear una estructura jerárquica clara que refleje la organización del proyecto
  • Pruebas basadas en resultados: Rust permite que las pruebas devuelvan un tipo Result<(), E>. Esto permite a los desarrolladores usar el operador ? dentro de las pruebas y gestionar los errores de forma más expresiva

Pruebas unitarias en TypeScript con Mocha y Chai

TypeScript se ha convertido en una opción popular para probar programas debido al dominio absoluto de Anchor como lingua franca del desarrollo en Rust para Solana. Con el comando anchor init, el framework de pruebas Mocha y la biblioteca de afirmaciones Chai se inicializan de forma predeterminada en los nuevos proyectos de Anchor. 

Mocha es un framework de pruebas de JavaScript con muchas funciones que se ejecuta en Node.js. Esto simplifica mucho las pruebas asincrónicas. El uso principal de Mocha en el desarrollo en Solana es probar la lógica del lado del cliente de las dApps y otras interacciones con la blockchain. 

Chai es una biblioteca de afirmaciones que puede combinarse con cualquier framework de pruebas de JavaScript, como Mocha. Ofrece a los desarrolladores diversas funciones para expresar afirmaciones de forma legible. Las interfaces expect, should e assert de Chai permiten escribir pruebas completas que resultan intuitivas tanto al leerlas como al escribirlas. Utiliza cadenas de lenguaje —es decir, getters encadenables— con las interfaces expect e should para mejorar la legibilidad de las afirmaciones. Gracias a Chai, escribir expect({a: 1, b: 2}).to.not.have.any.keys(“c”, “d”); produce una afirmación válida y muy legible.

Por ejemplo, si creamos un proyecto hello_world con el comando anchor init hello_world, se crea el siguiente archivo de prueba hello_world.ts en el directorio hello_world/tests:

Código
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloWorld } from "../target/types/hello_world";

describe("hello_world", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.HelloWorld as Program<HelloWorld>;

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
  });
});

Veamos qué significa todo esto.

Mocha utiliza bloques describe para agrupar pruebas y funciones it para definir casos de prueba. Este ejemplo sigue el patrón AAA para aplicar un enfoque estructurado:

  • Preparar: aquí, anchor.setProvider(anchor.AnchorProvider.env()); configura el cliente de Anchor para usar el proveedor predeterminado del entorno, que normalmente apunta a un validador de pruebas local de Solana. Luego, la declaración const program inicializa una instancia del programa que se probará, lo que nos permite llamar a sus métodos dentro de la prueba
  • Actuar: en nuestro caso de prueba “Is initialized!”, llamamos al método initialize de nuestro programa y enviamos la transacción
  • Afirmar: en este caso de prueba, registramos la firma de la transacción sin proporcionar ninguna afirmación. Normalmente, en esta fase incorporaríamos Chai para las afirmaciones. Como ejemplo muy básico, podríamos modificar el código de prueba predeterminado con una afirmación como expect(tx).to.be.a(“string”);. Una prueba más detallada podría recuperar e inspeccionar el estado del programa después de la inicialización y afirmar que coincide con los valores esperados

La combinación de Mocha y Chai con el patrón AAA configurado de forma predeterminada en los proyectos de Anchor ofrece un framework sólido para probar programas. Los desarrolladores de Solana pueden garantizar que los programas funcionen de forma predecible y confiable al preparar claramente el entorno de prueba, actuar invocando métodos del programa y afirmar los resultados.

Por ejemplo, imagina que estás desarrollando un programa que permite a los usuarios depositar y retirar SOL de una bóveda en Solana. Así podría verse una prueba escrita en TypeScript con Mocha y Chai para garantizar que la función de depósito opere según lo previsto:

Código
import { expect } from "chai";
import { PublicKey } from "@solana/web3.js";
import { depositSOL } from "../src/vault";

describe("Vault Program", function() {
    describe("Deposit functionality", function() {
        it("should correctly deposit SOL into the vault", async function() {
            const vaultPublicKey = new PublicKey(/* vault public key */);
            const userPublicKey = new PublicKey(/* user public key */);
            const depositAmount = 1; // 1 SOL

            const initialVaultBalance = await getVaultBalance(vaultPublicKey);
            await depositSOL(vaultPublicKey, userPublicKey, depositAmount);

            const finalVaultBalance = await getVaultBalance(vaultPublicKey);
            expect(finalVaultBalance).to.equal(initialVaultBalance + depositAmount);
        });
    });
});

Este ejemplo prueba una función hipotética depositSOL, que gestiona el depósito de SOL en una bóveda. Afirma que el saldo de la bóveda aumenta en la cantidad correcta después del depósito. Usamos la función getVaultBalance, una supuesta función auxiliar que obtiene el saldo actual de la bóveda.

Otras consideraciones importantes para las pruebas en TypeScript con Mocha y Chai

El sistema de tipos estático de TypeScript a veces puede complicar un poco la escritura de pruebas, especialmente al trabajar con tipos complejos o mal definidos. Usa afirmaciones de tipo para evitar problemas relacionados con los tipos en tus pruebas. Sin embargo, asegúrate de que estas afirmaciones no oculten posibles errores en tiempo de ejecución causados por tipos incorrectos.

Al simular objetos o funciones en TypeScript, asegúrate de que las entidades simuladas respeten los tipos correctos. Bibliotecas como ts-sinon o ts-mockito pueden ayudar a crear simulaciones con seguridad de tipos para que las pruebas sigan siendo precisas y reflejen el comportamiento real del programa.

Mocha proporciona los métodos only e skip para ejecutar de forma exclusiva u omitir pruebas específicas. Aunque son útiles durante el desarrollo, es fácil enviarlos accidentalmente a producción, lo que produce ejecuciones de pruebas incompletas. Revisa siempre que no haya only ni skip antes de enviar las pruebas a producción. Además, ten cuidado al usar los hooks de Mocha —es decir, beforeEach, afterEach, before, after— con código asincrónico. Asegúrate de gestionar correctamente las promesas mediante async/await o llama al método callback done para evitar promesas sin resolver o callbacks sin invocar.

Al usar expect().to.deep.equal() de Chai, considera su comportamiento con objetos que contienen propiedades generadas dinámicamente, como fechas o valores aleatorios. Estas propiedades pueden provocar fallos inesperados en pruebas que esperan una igualdad profunda. Cuando corresponda, considera usar expect().to.include() de Chai para realizar afirmaciones más específicas.

Frameworks populares para hacer pruebas en Solana

Bankrun

Un banco supervisa el seguimiento de las cuentas de los clientes, administra la ejecución de programas y mantiene la integridad y el avance del libro mayor de Solana. En esencia, es una instantánea del libro mayor en un momento determinado que encapsula el estado resultante de las transacciones de un bloque específico. 

Bankrun es un framework de pruebas ligero y flexible escrito en Node.js para programas de Solana. Prioriza la facilidad de uso y la velocidad, lo que permite a los desarrolladores escribir y ejecutar rápidamente pruebas de sus programas. El verdadero valor de Bankrun es que es un framework de pruebas que permite a los desarrolladores simular bancos de Solana e interactuar con ellos en un entorno controlado y eficiente. Bankrun agiliza el proceso de pruebas al reproducir la dinámica de los bancos de Solana sin la sobrecarga que suele implicar configurar un entorno de este tipo.

El diseño de Bankrun se basa en un BanksServer ligero que imita el comportamiento de un nodo RPC, pero ofrece un rendimiento y una flexibilidad considerablemente superiores. Los desarrolladores pueden interactuar con este servidor mediante el BanksClient. Este cliente ofrece un conjunto completo de herramientas con métodos para consultar saldos de cuentas y estados de transacciones, además de simular transacciones. En particular, el método tryProcessTransaction permite procesar transacciones que se espera que fallen sin generar errores de JavaScript. Esto permite a los desarrolladores verificar directamente modos de falla o mensajes de registro específicos.

El repositorio de Futarchy de Meta-DAO en GitHub es un excelente ejemplo del uso de Bankrun para probar código listo para producción.

Integración con Anchor

Integrar Bankrun con Anchor es muy sencillo. Con startAnchor, los desarrolladores pueden desplegar automáticamente en el entorno de pruebas todos los programas de un espacio de trabajo de Anchor. Esto garantiza que las pruebas reproduzcan con precisión el comportamiento del programa en un entorno completo de Solana. La documentación de Bankrun proporciona el siguiente ejemplo de código:

Código
import { startAnchor } from "solana-bankrun";
import { PublicKey } from "@solana/web3.js";

test("anchor", async () => {
	const context = await startAnchor("tests/anchor-example", [], []);
	const programId = new PublicKey(
		"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
	);
	const executableAccount = await context.banksClient.getAccount(programId);
	expect(executableAccount).not.toBeNull();
	expect(executableAccount?.executable).toBe(true);
});

El paquete anchor-bankrun es una potente extensión que integra Anchor y Bankrun al exportar una clase BankrunProvider que puede utilizarse como reemplazo directo de AnchorProvider durante las pruebas.

Escritura de cuentas arbitrarias con Bankrun

Una característica destacada de Bankrun es su capacidad para escribir datos arbitrarios en las cuentas. Esta funcionalidad permite a los desarrolladores superar las limitaciones de los estados de las cuentas al ofrecer un nivel de flexibilidad sin precedentes. Por ejemplo, un desarrollador puede simular una cuenta que contiene una cantidad considerable de USDC sin poseer el par de claves del mint de USDC. Esto resulta muy valioso para las pruebas, ya que elimina la necesidad de manipular tokens reales y agiliza la configuración de escenarios complejos. 

La documentación de Bankrun proporciona un ejemplo de código de un mint infinito de USDC, que demuestra esta capacidad mediante la función start. La función start prepara el entorno de pruebas al desplegar programas y configurar los datos de las cuentas según lo especificado.

Viajes en el tiempo

Otra característica independiente es la capacidad de Bankrun para viajar en el tiempo (es decir, manipular el concepto del tiempo con fines de prueba). La posibilidad de manipular el tiempo permite a los desarrolladores adelantar o retrasar el reloj del clúster de Solana (es decir, la sysvar Clock) para simular de inmediato condiciones temporales específicas. Esta capacidad es crucial para probar programas que funcionan con lógica basada en el tiempo, como calendarios de adquisición de derechos, bloqueos de tokens o cualquier funcionalidad que se active al alcanzar un momento determinado.

Viajar en el tiempo es sencillo gracias al método setClock. Este método permite a los desarrolladores establecer la hora actual del clúster en una marca de tiempo Unix predefinida y trasladar todo el entorno de pruebas a ese momento pasado o futuro. Las operaciones y transacciones de la prueba continúan como si la hora especificada fuera la actual, lo que permite evaluar con precisión el comportamiento del programa en esas condiciones.

Este es un ejemplo muy básico de cómo viajar en el tiempo con Bankrun:

Código
import { start } from "solana-bankrun";
import { PublicKey, Transaction, SystemProgram } from "@solana/web3.js";

async function simulateTimeTravel(context, secondsForward) {
    const newTimestamp = context.clock.unixTimestamp + secondsForward;
    context.adjustClock(newTimestamp);
}

test("One Year Later...", async () => {
    const context = await start([], []);
    const { banksClient, payer } = context;

    // Simulate setting the cluster clock forward by one year (in seconds)
    const oneYearInSeconds = 365 * 24 * 60 * 60;
    await simulateTimeTravel(context, oneYearInSeconds);

    // Proceed with tests assuming the future time
    const transaction = new Transaction().add(
        SystemProgram.transfer({
            fromPubkey: payer.publicKey,
            toPubkey: PublicKey.unique(),
            lamports: 100,
        }),
    );

    transaction.recentBlockhash = context.lastBlockhash;
    transaction.sign(payer);

    await banksClient.processTransaction(transaction);

    // Add assertions here to test expected future behavior
});

Bankrun vs. solana-test-validator

La elección entre Bankrun e solana-test-validator depende en gran medida de los requisitos específicos de los escenarios de prueba. La velocidad, flexibilidad y funcionalidad especializada de Bankrun lo convierten en la opción preferida para la mayoría de los escenarios de desarrollo, especialmente aquellos que requieren iteraciones rápidas o simulaciones detalladas. Sin embargo, solana-test-validator sigue siendo pertinente para pruebas que dependen del comportamiento de un validador real y del uso de métodos RPC que BanksServer no admite.

solana-program-test

El crate solana-program-test proporciona un framework de pruebas basado en Rust y diseñado específicamente para programas de Solana. Este framework se centra en BanksClient. Simula las operaciones de un banco de Solana y permite a los desarrolladores desplegar sus programas, interactuar con ellos y evaluar su comportamiento bajo condiciones de prueba que imitan la mainnet, de forma similar a Bankrun. Como complemento de BanksClient se encuentra la estructura ProgramTest, una utilidad para inicializar el entorno de pruebas. Es decir, facilita el desarrollo de programas específicos y la configuración de las cuentas necesarias. Otras estructuras, como BanksTransactionResultWithMetadata, InvokeContext y ProgramTestContext, ofrecen información y contexto detallados sobre las transacciones procesadas durante las pruebas, lo que mejora el proceso general de depuración y verificación. 

Para agilizar el desarrollo y las pruebas locales, solana-program-test precarga automáticamente varios programas:

  • SPL Token (y su versión de 2022)
  • SPL Memo (versiones 1.0 y 3.0)
  • SPL Associated Token Account

Estos programas precargados permiten una configuración de pruebas más rápida y específica, ya que no es necesario configurar manualmente estos programas comunes.

El repositorio de Marginfi en GitHub contiene varios ejemplos excelentes de implementación de solana-program-test en código listo para producción. La guía de desarrollo de Bonfida también incluye un excelente tutorial para escribir pruebas de integración con el framework solana-program-test.

solana-test-framework

solana-test-framework es una extensión de solana-program-test desarrollada por Halborn. Está diseñada para enriquecer el entorno de pruebas al ampliar BanksClient, RpcClient, ProgramTest y ProgramTestContext con varios métodos prácticos. Al igual que Bankrun, las extensiones de ProgramTestContext, por ejemplo, permiten escenarios de prueba avanzados en los que los desarrolladores pueden saltar a marcas de tiempo específicas y actualizar precios de oráculos.

Estas extensiones ofrecen las siguientes mejoras:

  • Administración de transacciones: Simplifica la creación, firma y el pago de transacciones mediante transaction_from_instructions
  • Deserialización de cuentas: Facilita la recuperación y deserialización de cuentas de Anchor y Borsh con get_account_with_anchor and get_account_with_borsh, respectivamente
  • Creación de cuentas y despliegue de programas: Permite a los desarrolladores configurar eficientemente su entorno de pruebas con funciones como create_account, create_token_mint, create_token_account y deploy_program.

solana-test-framework  admite tanto clústeres externos como un entorno de ejecución simulado. Es compatible con varias versiones de Solana y Anchor, incluidas las versiones de Solana de la 1.9 a la 1.14 y las versiones correspondientes de Anchor para 1.9, 1.10 y 1.14. 

Ejemplo de escenario de prueba

El programa

Toma como ejemplo el siguiente programa:

Código
use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;
use solana_program::program::invoke;

declare_id!("3vMZa7r3CpHGejvXYbUpPXmm54FxCDPF1QAYnnzL88J9");

#[program]
pub mod king_of_the_hill {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, initial_prize: u64) -> Result<()> {
        // In case the person who went first didn't send any SOL as the initial prize
        require!(initial_prize > 0, ErrorCode::NeedAnInitialPrize);

        let game_state = &mut ctx.accounts.game_state;

        game_state.king = ctx.accounts.initial_king.key();
        game_state.prize = initial_prize;

        let transfer_instruction = system_instruction::transfer(
            &ctx.accounts.initial_king.key(),
            &ctx.accounts.prize_pool.key(),
            initial_prize,
        );

        invoke(
            &transfer_instruction,
            &[
                ctx.accounts.initial_king.to_account_info(),
                ctx.accounts.prize_pool.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        Ok(())
    }

    pub fn become_king(ctx: Context<BecomeKing>, new_prize: u64) -> Result<()> {
        require!(
            new_prize > ctx.accounts.game_state.prize,
            ErrorCode::BidTooLow
        );

        let transfer_to_pool_instruction = system_instruction::transfer(
            &ctx.accounts.payer.key(),
            &ctx.accounts.prize_pool.key(),
            new_prize,
        );

        // Send the new king's funds to the pool
        invoke(
            &transfer_to_pool_instruction,
            &[
                ctx.accounts.payer.to_account_info(),
                ctx.accounts.prize_pool.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        // Send the old king's funds back
        ctx.accounts.prize_pool.sub_lamports(ctx.accounts.game_state.prize);
        ctx.accounts.king.add_lamports(ctx.accounts.game_state.prize);

        ctx.accounts.game_state.king = ctx.accounts.payer.key();
        ctx.accounts.game_state.prize = new_prize;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = initial_king,
        space = 8 + 32 + 8 + 1,
        seeds = [b"game_state"],
        bump,
    )]
    pub game_state: Account<'info, GameState>,
    #[account(mut)]
    pub initial_king: Signer<'info>,
    #[account(
        init,
        payer = initial_king,
        space = 8 + 8,
        seeds = [b"prize_pool"],
        bump,
    )]
    /// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
    pub prize_pool: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct BecomeKing<'info> {
    #[account(
        mut,
        has_one = king,
    )]
    pub game_state: Account<'info, GameState>,
    #[account(mut)]
    /// CHECK: This is okay - it's only receiving SOL and we don't need any other access
    pub king: UncheckedAccount<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        seeds = [b"prize_pool"],
        bump,
    )]
    /// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
    pub prize_pool: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct GameState {
    pub king: Pubkey,
    pub prize: u64,
    pub prize_pool_bump: u8,
}

#[error_code]
pub enum ErrorCode {
    #[msg("The initial prize must be greater than zero")]
    NeedAnInitialPrize,
    #[msg("The bid must be higher than the current prize")]
    BidTooLow,
    #[msg("Invalid prize pool account")]
    InvalidPrizePoolAccount,
}

Este programa implementa un sencillo juego de «Rey de la colina» en Solana. El juego permite que los usuarios se conviertan en el «rey» al enviar al fondo de premios más SOL que el rey actual. Cuando un nuevo rey ocupa su lugar, los SOL enviados por el rey anterior se le devuelven.

El programa funciona de la siguiente manera:

  • Inicializar: Esta función configura el juego con un rey inicial (es decir, el primer jugador que inicializa el juego) y un premio inicial. El premio inicial debe ser mayor que cero. Luego, la función transfiere el premio inicial del rey inicial a un fondo de premios
  • Convertirse en rey: Esta función permite que un nuevo jugador se convierta en rey al ofertar más SOL que el premio actual. Transfiere el premio actual al rey saliente y actualiza el fondo de premios con la oferta del nuevo rey, lo que lo convierte en el nuevo rey. Esta oferta debe ser mayor que el premio actual

Escritura de pruebas

Podemos probar correctamente el juego Rey de la colina con el siguiente código:

Código
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";

import { assert } from "chai";

const web3 = require("@solana/web3.js");

describe("King of the Hill Tests", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.KingOfTheHill
as Program<KingOfTheHill>;

  let initialKing, newKing;
  let gameStatePDA, prizePoolPDA;

  // Utility function for airdrops
  async function fundWallet(account, amount) {
    const publicKey = account.publicKey ? account.publicKey : account;

    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(publicKey, amount),
      "confirmed"
    );
  }

  before(async () => {
    initialKing = web3.Keypair.generate();
    newKing = web3.Keypair.generate();

    await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
    await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);

    [gameStatePDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("game_state")],
      program.programId
    );

    [prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("prize_pool")],
      program.programId
    );
  });

  it("Initializes the game correctly", async () => {
    // Arrange
    await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
    await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);

    let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);

    // Act
    const tx = await program.methods
      .initialize(initialPrize)
      .accounts({
        gameState: gameStatePDA,
        initialKing: initialKing.publicKey,
        prizePool: prizePoolPDA,
        systemProgram: web3.SystemProgram.programId,
      })
      .signers([initialKing])
      .rpc();

    // Assert
    let gameState: any = await program.account.gameState.fetch(gameStatePDA);
    assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
    assert.equal(
      gameState.prize.toString(),
      new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
    );
  });

  it("Changes the king correctly", async () => {
    // Arrange
    const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
    let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);

    // Act
    const becomeKingTx = await program.methods.becomeKing(newPrize)
      .accounts({
          gameState: gameStatePDA,
          king: initialKing.publicKey, // Correct usage of current king
          payer: newKing.publicKey, // New king who pays and becomes the king
          prizePool: prizePoolPDA,
          systemProgram: web3.SystemProgram.programId,
      })
      .signers([newKing]) // Signing by newKing
      .rpc();

    // Assert
    const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);

    const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
    assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");

    // Fetch the updated game state.
    const updatedGameState = await program.account.gameState.fetch(gameStatePDA);

    // Assertions to confirm the state has updated as expected.
    assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
    assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
  })
});

Analicemos cada parte.

Primero, comenzamos con nuestras importaciones y configuramos el entorno de pruebas en Anchor. Para este ejemplo, realizo las pruebas en TypeScript con Mocha y Chai en localhost:

Código
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";

import { assert } from "chai";

const web3 = require("@solana/web3.js");

Usamos describe para agrupar nuestros casos de prueba. También configuramos el cliente para que use el clúster local; configuramos correctamente el programa; inicializamos las variables para el rey inicial, el nuevo rey, el PDA del estado del juego y el PDA del fondo de premios, respectivamente; y creamos una función auxiliar para facilitar los airdrops:

Código
describe("King of the Hill Tests", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.KingOfTheHill as Program<KingOfTheHill>;

  let initialKing, newKing;
  let gameStatePDA, prizePoolPDA;

  // Utility function for airdrops
  async function fundWallet(account, amount) {
    const publicKey = account.publicKey ? account.publicKey : account;

    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(publicKey, amount),
      "confirmed"
    );
  }

// Other code

});

A continuación, usamos el hook before para configurar y financiar los pares de claves del rey inicial y del nuevo rey, además de derivar los PDA del estado del juego y del fondo de premios. Este bloque se ejecutará una vez antes de los casos de prueba, lo que nos permite simplificar cada caso y adaptarlo con mayor claridad al patrón AAA:

Código
before(async () => {
    initialKing = web3.Keypair.generate();
    newKing = web3.Keypair.generate();

    await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
    await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);

    [gameStatePDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("game_state")],
      program.programId
    );

    [prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("prize_pool")],
      program.programId
    );
});

El primer caso de prueba es muy sencillo: comprobamos si el juego se inicializa correctamente. En la preparación, financiamos los PDA del estado del juego y del fondo de premios para poder interactuar con ellos más adelante, y establecemos el premio inicial en 1 SOL. Después, ejecutamos la llamada al método initialize con initialPrize. Para las cuentas, pasamos el PDA del estado del juego, el rey inicial, el PDA del fondo de premios y el programa del sistema. El rey inicial firma esta acción. Por último, verificamos que el estado del juego haya actualizado correctamente el rey y el premio:

Código
it("Initializes the game correctly", async () => {
    // Arrange
    await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
    await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);

    let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);

    // Act
    const tx = await program.methods
      .initialize(initialPrize)
      .accounts({
        gameState: gameStatePDA,
        initialKing: initialKing.publicKey,
        prizePool: prizePoolPDA,
        systemProgram: web3.SystemProgram.programId,
      })
      .signers([initialKing])
      .rpc();

    // Assert
    let gameState: any = await program.account.gameState.fetch(gameStatePDA);
    assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
    assert.equal(
      gameState.prize.toString(),
      new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
    );
});

El siguiente caso de prueba garantiza que otra persona pueda convertirse en rey. En la preparación, consulta el saldo inicial del rey y establece el nuevo premio en 2 SOL. Luego, llama a la función becomeKing y pasa el importe más reciente del premio. Para las cuentas, pasamos el PDA del estado del juego, la clave pública del rey actual, el nuevo rey como pagador, el PDA del fondo de premios y el programa del sistema. El nuevo rey se configura como firmante. Para verificar el resultado, comprueba si el rey recibe los SOL iniciales que aportó para convertirse en rey y si el estado del juego se actualizó correctamente:

Código
it("Changes the king correctly", async () => {
    // Arrange
    const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
    let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);

    // Act
    const becomeKingTx = await program.methods.becomeKing(newPrize)
      .accounts({
          gameState: gameStatePDA,
          king: initialKing.publicKey, // Correct usage of current king
          payer: newKing.publicKey, // New king who pays and becomes the king
          prizePool: prizePoolPDA,
          systemProgram: web3.SystemProgram.programId,
      })
      .signers([newKing]) // Signing by newKing
      .rpc();

    // Assert
    const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);

    const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
    assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");

    // Fetch the updated game state.
    const updatedGameState = await program.account.gameState.fetch(gameStatePDA);

    // Assertions to confirm the state has updated as expected.
    assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
    assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
})

En este escenario de prueba, examinamos las funcionalidades principales de nuestro programa Rey de la colina. Mediante pruebas unitarias, validamos de forma granular la integridad de la lógica del programa. Al comprobar que el rey cambia correctamente, confirmamos además que el programa funciona según lo previsto en condiciones simuladas del mundo real. Estas pruebas subrayan la importancia de una estrategia de pruebas integral para garantizar la calidad y funcionalidad de nuestro programa Rey de la colina. 

Conclusión

Las pruebas son la base para desarrollar programas de Solana seguros, confiables y eficientes. En este artículo, exploramos la importancia de combinar pruebas unitarias, de integración y E2E para cubrir todas las etapas del ciclo de vida del desarrollo de programas. Al integrar estas metodologías y aprovechar potentes frameworks de pruebas como Bankrun, solana-program-test e solana-test-framework, los desarrolladores pueden mejorar considerablemente la calidad de sus programas de Solana. A medida que continúes tu trayectoria como desarrollador de Solana, usa los principios, las prácticas y los ejemplos que examinamos aquí como guía para crear programas sólidos, eficientes y seguros.

Si llegaste hasta aquí, ¡gracias, anon! Ingresa tu dirección de correo electrónico a continuación para no perderte ninguna novedad de Solana. ¿Quieres profundizar? Explora los artículos más recientes en el blog de Helius y continúa hoy mismo tu recorrido por Solana.

Recursos adicionales

Suscríbete a Helius

Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos

Imagen ampliada