Starting with Solana, Part 3 - Testing a Solana Program

Written

In part 2, we took a look at the account macros that Anchor uses to get data in and out of your Solana program. Now let’s go back to Nader Dabit’s tutorial and finish our first program.

To recap, the Rust source for the program looks like this.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod p0001 {
    use super::*;
    pub fn create(ctx: Context<Create>) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count += 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Create<'info> {
    #[account(init, payer=user, space= 8 + 8)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

Setting The Program ID 🔗

Again, the anchor build command will compile the program and generate a keypair that can be used to deploy it. Since Anchor’s template always has the same program ID at the top, we need to take the ID from the generated keypair and use it in our program.

The build output tells us where to find it.

The program address will default to this keypair (override with --program-id):
  /home/projects/solana/p0001/target/deploy/p0001-keypair.json

The keypair JSON file is actually an array of numbers, designed to conveniently translate into a byte array. But the solana CLI command can extract the public key – which is also the address – for us.

$ solana address -k target/deploy/p0001-keypair.json
6UrdK99AdvoFdM8WrteDfK9XS2ENSLEHFkXDSmMPryk

Here, your value should be different from the one I have. The address needs to go in two places:

  • The declare_id! macro at the top of our program’s source code.
  • The Anchor.toml file under the key with the same name as your program.
# Anchor.toml first section
[programs.localnet]
p0001 = "6UrdK99AdvoFdM8WrteDfK9XS2ENSLEHFkXDSmMPryk"

Writing a Test 🔗

Anchor generates a test file for us, but you may remember from part 1 that it doesn’t do much. Nader’s article creates a test that calls both of the instructions and verifies that they work, so let’s walk through that.

The shell of the test imports the Anchor framework JavaScript files and gets the program ready to run. The tests default to using the mocha test framework, so the it function defines a test and describe can be used to group tests together.

const assert = require('assert');
const anchor = require('@project-serum/anchor');
const { SystemProgram } = anchor.web3;

describe('p0001', () => {
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.p0001;
  // To be filled in by the create test and used by the increment test.
  let _baseAccount;

  // Tests here
});

We then have two tests, one for each instruction. More complex instructions might require multiple tests.

Testing the Create Instruction 🔗

The first test calls the create instruction, then gets the data for the created account and verifies that it exists and has a zero value for count.

it('Creates a counter', async () => {
  const baseAccount = anchor.web3.Keypair.generate();
  await program.rpc.create({
    accounts: {
      baseAccount: baseAccount.publicKey,
      user: provider.wallet.publicKey,
      systemProgram: SystemProgram.programId,
    },
    signers: [baseAccount],
  });

  // The account should have been created.
  const account = await program.account.baseAccount.fetch(
    baseAccount.publicKey
  );
  console.log('Initial count: ', account.count.toString());
  assert.ok(account.count.toString() == 0);
  _baseAccount = baseAccount;
});

Notice that although the system program has a fixed ID, we do still have to pass its address into the program.

Be sure that you are not running a local copy of solana-test-validator, and then use anchor test to run your test.

One oddity here is that count in our Rust program is a number, but the test calls toString() on it. Because JavaScript numbers are 64-bit floating point values, it can only accurately represent up to 53 bits worth of integers, so Anchor deserializes any value of 64 bits or above as a bn.js BigNum object.

> console.dir(account.count);
BN { negative: 0, words: [ 0, 0, 0 ], length: 1, red: null }

> console.log(account.count.toString());
0

Testing the Increment Instructions 🔗

The next test is for the increment instruction, which reuses the baseAccount from the first test.

it('Increments a counter', async () => {
  const baseAccount = _baseAccount;
  await program.rpc.increment({
    accounts: {
      baseAccount: baseAccount.publicKey,
    },
  });

  const account = await program.account.baseAccount.fetch(
    baseAccount.publicKey
  );
  console.log('After increment: ', account.count.toString());
  assert.ok(account.count.toString() == 1);
});

Not much to say about this one.

Deploy to Localhost 🔗

With solana-test-validator running on your local machine, you can deploy the contract.

$ anchor deploy
Deploying workspace: http://localhost:8899
Upgrade authority: /home/projects/.config/solana/id.json
Deploying program "p0001"...
Program path: /home/projects/solana/p0001/target/deploy/p0001.so...
Program Id: 6UrdK99AdvoFdM8WrteDfK9XS2ENSLEHFkXDSmMPryk

Deploy success

In part 4 we’ll take the lessons learned in these first three parts and create a todo list application with attached rewards. Check it out now!


Thanks for reading! If you have any questions or comments, please send me a note on Twitter.