Published on

Testing your Seahorse Programs

Authors

This tutorial follows on from the previous calculator app post.

Welcome back. Let's write some more Seahorse! Today we're going to look at testing. In the blockchain world testing is MUCH more important than traditional web development given there are many unfortunate case studies of small program bugs leading to huge loss of funds for users. Most Blockchains including Solana are 'permissionless' which essentially means they are open to all. Once your program is out in the wild, anyone can interact with it in any way they like, including ways unintended by the developers. Most, if not all, production code for popular applications will go through rigorous tests and be audited professionally, often multiple times.

As we are only beginning our journey into Solana development we don't need to worry about loss of funds yet. We will use testing as a way to check our programs are working as expected. As we know from previous tutorials, Seahorse compiles our code into an Anchor program written in Rust. The good news is that Anchor has built in testing capabilities that speed up the testing process. The bad news is those tests have to be written in JavaScript, there's no Python testing framework for Anchor. Hopefully someone will build it because let's be honest, no one really enjoys writing JavaScript.

Testing our Calculator App

Let's go back to our Calculator App from the previous tutorial here. CD into the main 'calculator' directory and you should see a folder called 'tests' and a file within that folder called 'calculator.ts', the TS extension indicates TypeScript, a subset of JavaScript. Open the file, delete the contents and let's start building a new typescript file.

First we import all the methods from anchor and destructure 3 methods BN, Program, web3, BN stands for Big Number. We declare 'assert' and then we import a Typescript version of the program's IDL which is auto generated for us by Anchor. Remember the IDL works kind of like an API for our program.

calculator.ts
import * as anchor from '@project-serum/anchor'
import { BN, Program, web3 } from '@project-serum/anchor'
const assert = require('assert')

import { Calculator } from '../target/types/calculator'

The main body of our test logic is wrapped up in a single describe function. Within this wrapper we start with some boilerplate declaring the provider.

calculator.ts
describe('calculator', () => {

  const provider = anchor.AnchorProvider.env()
  anchor.setProvider(provider)

  // TESTS GO IN HERE

})

Next we declare the program, the owner and the calculator account address associated with the owner. Remember the owner's calculator address is derived from 2 seeds, which was the string 'Calculator' and the owner's address.

calculator.ts
  const program = anchor.workspace.Calculator as Program<Calculator>
  const owner = provider.wallet.publicKey
  const calculator = web3.PublicKey.findProgramAddressSync(
    [Buffer.from('Calculator'), owner.toBuffer()],
    program.programId
  )[0]

We're ready for our first test. Tests are always wrapped in it(). They use the async/await pattern common to asynchronous functions in JS, which makes sense because we are sending a transaction to the blockchain, it will take some time for us to receive a reply. In our first test we are calling the init function passing in 2 arguments, the owner and the calculator address which we've previously derived. rpc() is what we use to send the transaction to the Solana blockchain through a RPC (Remote Procedure Call).

calculator.ts
  it('Inits a calculator', async () => {
    await program.methods.initCalculator().accounts({ owner, calculator }).rpc()
  })

Next we're going to test the addition, multiplication and subtraction functions. These 3 will be done together in 1 block, so wrapped in a single it(). Notice how the operation is written as such .doOperation({ add: true }, new BN(2)) BN here is used for us to pass in the integer 2, 'add' is passed in a boolean key-value pair.

After the operations, we fetch the calculator's state on the chain and check if the 'display' value is what we expect, which is 5.

calculator.ts
 it('Does some operations', async () => {
    const add2 = await program.methods
      .doOperation({ add: true }, new BN(2))
      .accounts({ owner, calculator })
      .instruction()


    const mul3 = await program.methods
      .doOperation({ mul: true }, new BN(3))
      .accounts({ owner, calculator })
      .instruction()


    const sub1 = await program.methods
      .doOperation({ sub: true }, new BN(1))
      .accounts({ owner, calculator })
      .instruction()

    const tx = new web3.Transaction()
    tx.add(add2, mul3, sub1)
    await provider.sendAndConfirm(tx)

    // Get the calculator's on-chain data
    const calculatorAccount = await program.account.calculator.fetch(calculator)

    assert.ok(calculatorAccount.display.toNumber() === 5)
  })

Our final check is a bit different. Here we create a fresh address with new web3.Keypair() labeled 'hackerman' which mimics a nefarious actor trying to mess with our program. We create a transaction using their keypair and our calculator, this should fail because the calculator does not belong to them. We assert this does in fact fail with the line .then(() => assert.ok(false)).

calculator.ts
  it('Prevents fraudulent transactions', async () => {
    let hackerman = new web3.Keypair()

    let shouldFail = await program.methods
      .resetCalculator()
      .accounts({
        owner: hackerman.publicKey,
        calculator,
      })
      .instruction()

    let tx = new web3.Transaction()
    tx.add(shouldFail)
    await provider
      .sendAndConfirm(tx, [hackerman])
      .then(() => assert.ok(false)) // Error on success, we want a failure
      .catch(() => {})
  })

And that's it. The final code should look something like this below.

calculator.ts
import * as anchor from '@project-serum/anchor'
import { BN, Program, web3 } from '@project-serum/anchor'
const assert = require('assert')


import { Calculator } from '../target/types/calculator'


describe('calculator', () => {
  // Run some tests on our calculator program
  const provider = anchor.AnchorProvider.env()
  anchor.setProvider(provider)

  const program = anchor.workspace.Calculator as Program<Calculator>

  // Set up some common accounts we'll be using later
  const owner = provider.wallet.publicKey
  const calculator = web3.PublicKey.findProgramAddressSync(
    [Buffer.from('Calculator'), owner.toBuffer()],
    program.programId
  )[0]


  // Try initializing the calculator
  it('Inits a calculator', async () => {
    await program.methods.initCalculator().accounts({ owner, calculator }).rpc()
  })

  // Do some operations on the calculator
  it('Does some operations', async () => {
    const add2 = await program.methods
      .doOperation({ add: true }, new BN(2))
      .accounts({ owner, calculator })
      .instruction()


    const mul3 = await program.methods
      .doOperation({ mul: true }, new BN(3))
      .accounts({ owner, calculator })
      .instruction()


    const sub1 = await program.methods
      .doOperation({ sub: true }, new BN(1))
      .accounts({ owner, calculator })
      .instruction()

    const tx = new web3.Transaction()
    tx.add(add2, mul3, sub1)
    await provider.sendAndConfirm(tx)

    // Get the calculator's on-chain data
    const calculatorAccount = await program.account.calculator.fetch(calculator)

    assert.ok(calculatorAccount.display.toNumber() === 5)
  })

  // Make sure our calculator is secure
  it('Prevents fraudulent transactions', async () => {
    let hackerman = new web3.Keypair()

    let shouldFail = await program.methods
      .resetCalculator()
      .accounts({
        owner: hackerman.publicKey,
        calculator,
      })
      .instruction()

    let tx = new web3.Transaction()
    tx.add(shouldFail)
    await provider
      .sendAndConfirm(tx, [hackerman])
      .then(() => assert.ok(false)) // Error on success, we want a failure
      .catch(() => {})
  })
})

To run the test. Make sure you are in the base directory for your Anchor project, (i.e. /calculator). We simply run

anchor test

And that's it. We've tested our calculator program. Congrats!