| name | anchor-testing |
| description | Testing strategies for Anchor programs including unit tests, integration tests, and transaction simulation. Load when writing or reviewing tests for Anchor programs. |
Anchor Testing
Testing patterns and strategies for Anchor programs using TypeScript/JavaScript clients. Covers test setup, account creation, instruction calls, assertions, and error testing.
Quick Reference
Basic Test Structure
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { expect } from "chai";
import { MyProgram } from "../target/types/my_program";
describe("my-program", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.MyProgram as Program<MyProgram>;
it("Initializes account", async () => {
});
});
Calling Instructions
await program.methods
.initialize(arg1, arg2)
.accounts({
account1: publicKey1,
account2: publicKey2,
})
.signers([keypair])
.rpc();
PDA Derivation in Tests
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("seed"), user.publicKey.toBuffer()],
program.programId,
);
Fetching and Asserting
const account = await program.account.myAccount.fetch(publicKey);
expect(account.value).to.equal(42);
Test Environment Setup
Source: Working examples from Anchor Book
Provider and Program Access
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MyProgram } from "../target/types/my_program";
describe("my-program", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.MyProgram as Program<MyProgram>;
const wallet = provider.wallet;
});
What provider gives you:
- Connection to Solana cluster
- Wallet for signing transactions
- Configuration for tests
Accessing Multiple Programs
const puppetProgram = anchor.workspace.Puppet as Program<Puppet>;
const puppetMasterProgram = anchor.workspace
.PuppetMaster as Program<PuppetMaster>;
Use when: Testing CPIs between programs
Account Setup
Generating Keypairs
import { Keypair } from "@solana/web3.js";
const myAccount = Keypair.generate();
const authority = Keypair.generate();
Use for: Accounts that need to be created (not PDAs)
Deriving PDAs
const [userStatsPDA, bump] = await PublicKey.findProgramAddress(
[Buffer.from("user-stats"), user.publicKey.toBuffer()],
program.programId,
);
Seed encoding:
- Strings:
Buffer.from("string") or anchor.utils.bytes.utf8.encode("string")
- Pubkeys:
.toBuffer()
- Numbers:
Buffer.from(num.toString()) or converted to bytes
Complete working example source: Anchor Book - PDA Test
Funding Test Accounts
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
await provider.connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL);
await new Promise((resolve) => setTimeout(resolve, 1000));
Calling Instructions
Basic Pattern
await program.methods
.initialize(arg1, arg2)
.accounts({
account1: publicKey1,
account2: publicKey2,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
With Signers
await program.methods
.initialize()
.accounts({
newAccount: myKeypair.publicKey,
user: provider.wallet.publicKey,
})
.signers([myKeypair])
.rpc();
Transaction vs RPC
await program.methods.initialize().accounts({...}).rpc();
const tx = await program.methods.initialize().accounts({...}).transaction();
Testing PDAs
Source: Anchor Book - PDA Testing
Complete PDA Test Example
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { Game } from "../target/types/game";
import { expect } from "chai";
describe("game", async () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Game as Program<Game>;
it("Sets and changes name!", async () => {
const [userStatsPDA, _] = await PublicKey.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("user-stats"),
provider.wallet.publicKey.toBuffer(),
],
program.programId,
);
await program.methods
.createUserStats("brian")
.accounts({
user: provider.wallet.publicKey,
userStats: userStatsPDA,
})
.rpc();
expect((await program.account.userStats.fetch(userStatsPDA)).name).to.equal(
"brian",
);
await program.methods
.changeUserName("tom")
.accounts({
user: provider.wallet.publicKey,
userStats: userStatsPDA,
})
.rpc();
expect((await program.account.userStats.fetch(userStatsPDA)).name).to.equal(
"tom",
);
});
});
Key patterns:
- PDA derived client-side
- Same PDA used for init and update
- Fetch account to verify state changes
Testing CPIs
Source: Anchor Book - CPI Testing
Multi-Program Test
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Keypair } from "@solana/web3.js";
import { expect } from "chai";
import { Puppet } from "../target/types/puppet";
import { PuppetMaster } from "../target/types/puppet_master";
describe("puppet", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const puppetProgram = anchor.workspace.Puppet as Program<Puppet>;
const puppetMasterProgram = anchor.workspace
.PuppetMaster as Program<PuppetMaster>;
const puppetKeypair = Keypair.generate();
it("Does CPI!", async () => {
await puppetProgram.methods
.initialize()
.accounts({
puppet: puppetKeypair.publicKey,
user: provider.wallet.publicKey,
})
.signers([puppetKeypair])
.rpc();
await puppetMasterProgram.methods
.pullStrings(new anchor.BN(42))
.accounts({
puppetProgram: puppetProgram.programId,
puppet: puppetKeypair.publicKey,
})
.rpc();
expect(
(
await puppetProgram.account.data.fetch(puppetKeypair.publicKey)
).data.toNumber(),
).to.equal(42);
});
});
Validates:
- CPI correctly invokes target program
- State changes persist across programs
- Correct program IDs used
Fetching Account Data
Fetch Account
const account = await program.account.myAccount.fetch(publicKey);
Returns: Deserialized account data matching your #[account] struct
Fetch Multiple Accounts
const accounts = await program.account.myAccount.all();
Returns: Array of all accounts of this type
Fetch with Filters
const accounts = await program.account.myAccount.all([
{
memcmp: {
offset: 8,
bytes: anchor.utils.bytes.bs58.encode(authorityPublicKey.toBuffer()),
},
},
]);
Use for: Finding accounts by field values
Assertions
Using Chai
import { expect } from "chai";
expect(account.value).to.equal(42);
expect(account.authority.toString()).to.equal(authority.publicKey.toString());
expect(account.initialized).to.be.true;
expect(account.balance).to.be.greaterThan(0);
expect(account.data).to.deep.equal({ field: value });
BigNumber Assertions
expect(account.amount.toNumber()).to.equal(1000);
expect(account.amount.eq(new anchor.BN(1000))).to.be.true;
Error Testing
Expecting Errors
import { assert } from "chai";
try {
await program.methods
.invalidOperation()
.accounts({...})
.rpc();
assert.fail("Expected error was not thrown");
} catch (error) {
expect(error.message).to.include("custom error message");
}
Testing Anchor Errors
it("Fails with InvalidAmount error", async () => {
try {
await program.methods
.transfer(0)
.accounts({...})
.rpc();
assert.fail("Should have thrown InvalidAmount error");
} catch (error) {
expect(error.error.errorCode.code).to.equal("InvalidAmount");
expect(error.error.errorCode.number).to.equal(6000);
expect(error.error.errorMessage).to.include("Amount must be greater than zero");
}
});
Testing Constraint Failures
it("Fails when unauthorized", async () => {
const unauthorizedKeypair = Keypair.generate();
try {
await program.methods
.restrictedOperation()
.accounts({
account: accountPubkey,
authority: unauthorizedKeypair.publicKey,
})
.signers([unauthorizedKeypair])
.rpc();
assert.fail("Should have failed authorization check");
} catch (error) {
expect(error).to.exist;
}
});
Testing Patterns for Escrow
Testing Escrow Creation
it("Creates escrow account", async () => {
const index = new anchor.BN(0);
const [escrowPDA] = await PublicKey.findProgramAddress(
[
Buffer.from("escrow"),
owner.publicKey.toBuffer(),
index.toArrayLike(Buffer, "le", 8),
],
program.programId,
);
await program.methods
.createEscrow(facilitator.publicKey, refundTimeout, deadmanTimeout)
.accounts({
escrow: escrowPDA,
owner: owner.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([owner])
.rpc();
const escrow = await program.account.escrowAccount.fetch(escrowPDA);
expect(escrow.owner.toString()).to.equal(owner.publicKey.toString());
expect(escrow.facilitator.toString()).to.equal(
facilitator.publicKey.toString(),
);
expect(escrow.pendingCount.toNumber()).to.equal(0);
});
Testing Session Key Registration
it("Registers session key", async () => {
const sessionKey = Keypair.generate();
const [sessionKeyPDA] = await PublicKey.findProgramAddress(
[
Buffer.from("session"),
escrowPDA.toBuffer(),
sessionKey.publicKey.toBuffer(),
],
program.programId,
);
await program.methods
.registerSessionKey(
sessionKey.publicKey,
null,
1000,
)
.accounts({
escrow: escrowPDA,
sessionKey: sessionKeyPDA,
owner: owner.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([owner])
.rpc();
const sessionKeyAccount =
await program.account.sessionKey.fetch(sessionKeyPDA);
expect(sessionKeyAccount.active).to.be.true;
});
Testing Authorization Expiry
Replay protection relies on two mechanisms: PDA init uniqueness prevents duplicate authorization_id values while pending, and expires_at_slot prevents replay after finalization.
it("Rejects expired authorization", async () => {
const authorizationId = new anchor.BN(12345);
const expiredSlot = new anchor.BN(0);
try {
await program.methods
.submitAuthorization(, authorizationId, expiredSlot, )
.accounts({...})
.rpc();
assert.fail("Should have rejected expired authorization");
} catch (error) {
expect(error.error.errorCode.code).to.equal("AuthorizationExpired");
}
});
it("Rejects duplicate authorization_id while pending", async () => {
const authorizationId = new anchor.BN(99999);
const validExpiresAt = new anchor.BN(currentSlot + 100);
await program.methods
.submitAuthorization(, authorizationId, validExpiresAt, )
.accounts({...})
.rpc();
try {
await program.methods
.submitAuthorization(, authorizationId, validExpiresAt, )
.accounts({...})
.rpc();
assert.fail("Should have rejected duplicate authorization_id");
} catch (error) {
expect(error).to.exist;
}
});
Testing Time-Based Logic
it("Enforces refund window", async () => {
await program.methods
.submitAuthorization()
.accounts({...})
.rpc();
try {
await program.methods
.finalize()
.accounts({...})
.rpc();
assert.fail("Should enforce refund window");
} catch (error) {
expect(error.error.errorCode.code).to.equal("RefundWindowNotExpired");
}
});
Test Organization
Grouping Related Tests
describe("escrow-program", () => {
describe("Escrow Creation", () => {
it("Creates escrow with correct parameters", async () => {});
it("Fails when facilitator is invalid", async () => {});
});
describe("Session Keys", () => {
it("Registers session key", async () => {});
it("Revokes session key", async () => {});
it("Enforces grace period", async () => {});
});
describe("Settlements", () => {
it("Submits authorization", async () => {});
it("Finalizes after refund window", async () => {});
it("Processes refunds", async () => {});
});
});
Setup and Teardown
describe("escrow-program", () => {
let escrowPDA: PublicKey;
let owner: Keypair;
let facilitator: Keypair;
before(async () => {
owner = Keypair.generate();
facilitator = Keypair.generate();
});
beforeEach(async () => {
escrowPDA = await createEscrow(owner, facilitator);
});
it("Test 1", async () => {});
it("Test 2", async () => {});
});
Reusable Test Helpers
async function createEscrow(
owner: Keypair,
facilitator: Keypair,
): Promise<PublicKey> {
const index = new anchor.BN(0);
const [escrowPDA] = await PublicKey.findProgramAddress(
[
Buffer.from("escrow"),
owner.publicKey.toBuffer(),
index.toArrayLike(Buffer, "le", 8),
],
program.programId,
);
await program.methods
.createEscrow(facilitator.publicKey, 100, 1000)
.accounts({
escrow: escrowPDA,
owner: owner.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([owner])
.rpc();
return escrowPDA;
}
async function registerSessionKey(
escrowPDA: PublicKey,
owner: Keypair,
): Promise<PublicKey> {
}
Testing Frameworks
Source: Anchor Testing Documentation
Default: Mocha + Chai
Anchor projects use Mocha by default:
{
"scripts": {
"test": "anchor test"
}
}
LiteSVM
Fast testing with minimal dependencies:
Mollusk
Rust-native testing (not TypeScript):
Best Practices
Test Coverage
Cover these scenarios:
- Happy path: Normal operation
- Edge cases: Boundary conditions
- Error cases: Invalid inputs, unauthorized access
- State transitions: Account lifecycle
- Security: Authorization, validation
Independent Tests
Each test should:
- Set up its own state
- Not depend on other tests
- Clean up after itself (or use fresh accounts)
Clear Test Names
it("Rejects expired authorization", async () => {});
it("Transfers tokens to merchant after refund window", async () => {});
it("Test 1", async () => {});
it("Works", async () => {});
Arrange-Act-Assert Pattern
it("Updates account value", async () => {
const account = await createAccount();
const newValue = 42;
await program.methods
.update(newValue)
.accounts({ account: account.publicKey })
.rpc();
const updatedAccount = await program.account.data.fetch(account.publicKey);
expect(updatedAccount.value).to.equal(newValue);
});
Skill Loading Guidance
Load This Skill When
- Writing tests for Anchor programs
- Reviewing test coverage
- Debugging test failures
- Setting up test infrastructure
- Testing complex scenarios (escrow, vaults, etc.)
Related Skills
- anchor-core - For understanding program structure being tested
- anchor-pdas - For testing PDA derivation
- anchor-cpis - For testing cross-program invocations
- anchor-token-operations - For testing token operations
Reference Links
Official Documentation
Source Material
Testing Frameworks
Acknowledgment
At the start of a session, after reviewing this skill, state: "I have reviewed the anchor-testing skill and understand how to write comprehensive tests for Anchor programs."