Hello! In our last lesson, we focused on creating new accounts using Anchor's init constraint. We even got a sneak peek at how to write a TypeScript test for that specific initialize instruction.
This lesson will build directly on that foundation. Our goal is to formalize your understanding of testing and empower you to write and run comprehensive integration tests for any Anchor program using its TypeScript client. We'll cover the full testing workflow, from setting up the environment to writing assertions that verify your on-chain logic.
Given your extensive experience with frontend development and testing frameworks, you'll find the tools and patterns used in Anchor testing—namely TypeScript, Mocha, and Chai—very familiar. This lesson will focus on applying those skills in the unique context of the Solana blockchain.
Unit Tests vs. Integration Tests
In software development, we often distinguish between different types of tests. In Solana, the two most common are unit tests and integration tests.
- Unit Tests are written in Rust and live alongside your program code. They are designed to test small, isolated pieces of logic (e.g., a single helper function) without involving the Solana runtime. They are fast and great for testing pure logic.
- Integration Tests are written in TypeScript/JavaScript and live in the
tests/directory of your Anchor project. Their purpose is to test how your program behaves as a whole when deployed on a live (or simulated) Solana cluster. They test the interaction between instructions, accounts, and the Solana runtime itself.
When you run the anchor test command, you are executing these integration tests.
Integration tests and Unit tests
To start, let's clarify the distinction between these two testing methodologies. The document "Integration tests and Unit tests" from the Ackee Blockchain Solana Auditor's Bootcamp provides a concise table that perfectly summarizes the differences.
Please read the short section titled "Unit tests and Integration tests". Focus on the table comparing the purpose, scope, and use cases of each test type.
The Anchor Testing Workflow
Anchor provides a powerful, streamlined workflow for integration testing. The central command is anchor test. When you run it, Anchor performs several steps automatically:
- Starts a local Solana test validator: It creates a fresh, local blockchain instance for your tests.
- Builds your program: It compiles your Rust code into a Solana-compatible BPF (Berkeley Packet Filter) binary.
- Generates the IDL: It creates or updates the Interface Definition Language (IDL) file in
target/idl/. This JSON file describes your program's instructions, accounts, and custom types. - Deploys the program: It deploys your BPF binary to the local test validator.
- Runs your tests: It executes the TypeScript test files located in your project's
tests/directory using the Mocha test runner.
Solana Smart Contract Tutorial: Using the Anchor Framework
The video "Solana Smart Contract Tutorial: Using the Anchor Framework" by Josh's DevBox provides a good overview of the key Anchor commands, including anchor test.
Watch the segment from 00:02:22 to 00:03:36. This will give you a quick recap of the main commands in the Anchor CLI and situate anchor test within the development cycle.
Setting Up the Test Environment
Your test environment is primarily configured in two places: Anchor.toml and your TypeScript test file.
1. Anchor.toml Configuration
This file tells Anchor how to run your tests. The [scripts] section is key:
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
This line specifies the command to execute your tests. By default, it uses ts-mocha to find and run all files ending in .ts inside the tests/ directory.
You can also configure the behavior of the local validator under the [test.validator] section, for example, to airdrop SOL to a specific address at genesis or to clone accounts from a public cluster.
2. The TypeScript Test File
Anchor test files use a standard structure that will be familiar from modern JavaScript/TypeScript testing.
- Mocha: The test framework that provides the
describe()andit()blocks to structure your tests.describegroups related tests, anditdefines an individual test case. - Chai: The assertion library used to verify outcomes. While Anchor examples often use
assert, you can also useexpectorshouldstyles. @coral-xyz/anchor: The library that provides the tools to connect to the cluster and interact with your program.
A Guide to Testing Solana Programs
The article "A Guide to Testing Solana Programs" by Helius offers a clear explanation of how Mocha and Chai are used in the default Anchor setup. It also introduces the Arrange-Act-Assert (AAA) pattern, a great structure for writing clean tests.
Read the section "Unit Testing in TypeScript with Mocha and Chai". This will explain the roles of describe and it and how to structure a test case.
Writing a Test Suite: The Core Pattern
Let's break down how to write a typical test file, following the Arrange-Act-Assert pattern.
Arrange: Setting the Stage
Before you can call an instruction, you need to set up all the necessary context.
-
Get the Provider and Program:
anchor.AnchorProvider.env()creates a connection to the Solana cluster based on yourAnchor.tomlconfiguration.anchor.workspace.YourProgramNameprovides a typed client object for interacting with your specific program. This object is automatically generated by Anchor using the IDL, giving you type-safe access to your program's methods.
-
Create Keypairs:
- You'll often need to generate new keypairs to represent users or new accounts. You do this with
anchor.web3.Keypair.generate().
- You'll often need to generate new keypairs to represent users or new accounts. You do this with
-
Fund Wallets:
- Transactions on Solana cost SOL for fees, and creating accounts requires SOL for rent. You must ensure your payer accounts have sufficient funds. A common pattern is to create a helper function to airdrop SOL.
Here's a typical setup block using Mocha's before hook, which runs once before all tests in the describe block.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { assert } from "chai";
import { MyProgram } from "../target/types/my_program";
describe("my_program", () => {
// --- ARRANGE ---
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.MyProgram as Program<MyProgram>;
// A keypair for the account we'll create in our tests
const counterAccount = anchor.web3.Keypair.generate();
// The main user/payer for the tests
const payer = provider.wallet;
// This runs once before all tests
before(async () => {
// Airdrop SOL to our payer, just in case
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(payer.publicKey, 2 * anchor.web3.LAMPORTS_PER_SOL),
"confirmed"
);
});
// ... your 'it' blocks go here
});
Act: Calling the Instruction
To call a program instruction, you use the program.methods object. The syntax is a chain of method calls:
program.methods.instructionName(...args).accounts({...}).signers([...]).rpc()
.methods.instructionName(...args): This corresponds to your Rust instruction handler. Any arguments the Rust function takes (besidesContext) are passed here. Note that numbers likeu64must be wrapped innew anchor.BN()..accounts({...}): You provide the public keys of all the accounts required by the instruction'sAccountsstruct..signers([...]): An array of the fullKeypairobjects that need to sign the transaction. This includes thepayerand, as we saw in the last lesson, any new accounts being created withinit..rpc(): This sends the transaction to the network and waits for confirmation.
Assert: Verifying the Result
After the transaction is confirmed, you need to check that the program's state changed as expected.
- Fetch the Account Data: You can retrieve the latest state of an account using
program.account.accountStructName.fetch(publicKey). - Make Assertions: Use Chai's
assert(orexpect) to compare the fetched data with your expected values.
Here's a complete it block for an initialize instruction:
it("Is initialized!", async () => {
// --- ACT ---
await program.methods
.initialize()
.accounts({
counter: counterAccount.publicKey,
user: payer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([counterAccount]) // The new account must sign for `init`
.rpc();
// --- ASSERT ---
const account = await program.account.counter.fetch(counterAccount.publicKey);
assert.ok(account.count.toNumber() === 0);
});
And for a subsequent increment instruction:
it("Increments the count", async () => {
// --- ACT ---
await program.methods
.increment()
.accounts({
counter: counterAccount.publicKey,
})
.rpc();
// --- ASSERT ---
const account = await program.account.counter.fetch(counterAccount.publicKey);
assert.ok(account.count.toNumber() === 1);
});
Test your understanding!
In the increment test above, why don't we need to pass anything to .signers()?
Show answer
The increment instruction modifies the counter account, but the transaction is paid for by the default wallet/payer configured in the provider (provider.wallet). Anchor automatically includes this default wallet as a signer.
The Rust Accounts struct for increment would not require any additional signers (unlike initialize, which needs the new account's signature). Therefore, no extra signers are needed in the .signers([]) array.
Walkthrough: Building a Test Suite
Now, let's see all these pieces come together. The following video is a fantastic, detailed walkthrough of creating a test suite for a simple counter program. It covers everything from setting up the describe block to testing initialize, update, increment, and decrement instructions.
Solana Smart Contract Tutorial: Using the Anchor Framework
This extended segment from Josh's DevBox is a practical guide to writing a full test suite. It will solidify your understanding of the entire process.
Please watch from 00:29:24 to 00:42:11. This covers: Setting up the test file: How describe and it are used, and how to configure the environment with anchor.workspace. Testing initialize: Creating a keypair for the data account and asserting the initial state. Passing arguments: Testing an update function that takes a u64 argument, showing how to use new anchor.BN(). Testing state changes: Writing tests for increment and decrement and asserting the final count.
This video clearly illustrates the "Arrange, Act, Assert" pattern for each instruction and highlights common pitfalls, like forgetting to use anchor.BN for u64 types.
Conclusion
You now have a solid framework for testing your Anchor programs. Because of your background, this TypeScript-based workflow should feel natural, allowing you to focus on the unique aspects of on-chain testing.
Here are your key takeaways:
anchor testis the primary command for running integration tests, which validate your program's on-chain behavior.- Tests are written in TypeScript using Mocha (
describe,it) for structure and Chai (assert) for verification. - The Arrange-Act-Assert pattern provides a clean structure for test cases.
- Arrange: Set up the provider, program client, and create/fund necessary keypairs.
- Act: Call program methods using the
program.methods.name(...).accounts(...).signers(...).rpc()chain. - Assert: Fetch the updated account state with
program.account.name.fetch(...)and verify its properties.
- Anchor's
workspaceand typed program client, powered by the IDL, provide a safe and efficient way to interact with your program from your tests.
In our next lesson, we will dive into a concept we've already used implicitly: Cross-Program Invocations (CPIs). You've seen Anchor use a CPI to the System Program via the init constraint. Next, we'll learn how to perform these program-to-program calls manually and explicitly, unlocking a vast new range of capabilities for your Solana programs.
Can't find a good explanation? Sign up and we'll make it for you
Sign up