In this lesson, you will:
This lesson guides writing tests for a Solana program that incorporates cross-program invocation (CPI). It begins with setting up the testing environment and generating key pairs for data accounts. The tests involve initializing a data account for the lever program, executing a virtual lever pull-through CPI, and verifying the updated state value. These tests aim to ensure the program's functionality.
To start, clear the existing code in the test file and replace it with the following code block:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Lever } from "../target/types/lever";
import { Hand } from "../target/types/hand";
describe("cross-program-invocation", () => {
// Configuration for the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// Generate keypairs for data accounts of each program.
const dataAccountLever = anchor.web3.Keypair.generate();
const dataAccountHand = anchor.web3.Keypair.generate();
const wallet = provider.wallet;
// Lever and Hand programs
const leverProgram = anchor.workspace.Lever as Program<Lever>;
const handProgram = anchor.workspace.Hand as Program<Hand>;
it("Initialize the lever!", async () => {
// Initialize data account for lever program
const tx = await leverProgram.methods
.new()
.accounts({ dataAccount: dataAccountLever.publicKey })
.signers([dataAccountLever])
.rpc();
console.log("Your transaction signature", tx);
// Fetch and display the state of the data account
const val = await leverProgram.methods
.get()
.accounts({ dataAccount: dataAccountLever.publicKey })
.view();
console.log("State:", val);
});
it("Pull the lever!", async () => {
// Initialize data account for hand program
const tx = await handProgram.methods
.new()
.accounts({ dataAccount: dataAccountHand.publicKey })
.signers([dataAccountHand])
.rpc();
console.log("Your transaction signature", tx);
// Invoke pullLever in hand program, calling lever program via CPI
const tx2 = await handProgram.methods
.pullLever(dataAccountLever.publicKey, "Chris")
.accounts({ dataAccount: dataAccountHand.publicKey })
.remainingAccounts([
// Lever program's data account
{
pubkey: dataAccountLever.publicKey,
isWritable: true,
isSigner: false,
},
// Lever program's ID
{
pubkey: leverProgram.programId,
isWritable: false,
isSigner: false,
},
])
.rpc({ skipPreflight: true });
console.log("Your transaction signature", tx2);
// Fetch and display updated state
const val = await leverProgram.methods
.get()
.accounts({ dataAccount: dataAccountLever.publicKey })
.view();
console.log("State:", val);
});
it("Pull it again!", async () => {
// Repeat lever pull with different arguments
const tx = await handProgram.methods
.pullLever(dataAccountLever.publicKey, "Ashley")
.accounts({ dataAccount: dataAccountHand.publicKey })
.remainingAccounts(
// Lever program's data account
{
pubkey: dataAccountLever.publicKey,
isWritable: true,
isSigner: false,
},
// Lever program's ID
{
pubkey: leverProgram.programId,
isWritable: false,
isSigner: false,
}
)
.rpc({ skipPreflight: true });
console.log("Your transaction signature", tx);
// Fetch and display updated state
const val = await leverProgram.methods
.get()
.accounts({ dataAccount: dataAccountLever.publicKey })
.view();
console.log("State:", val);
});
});
In this code:
describe
block outlines our test suite for the CPI program.describe("cross-program-invocation", () => {
})
In this part, we'll talk about and test how the "CPI" program works. We'll write different test cases, make assertions, and set expectations to make sure the program behaves correctly. For this, you need to first set the Requirements for the test:
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// Generate new keypairs for data accounts
const dataAccountLever = anchor.web3.Keypair.generate();
const dataAccountHand = anchor.web3.Keypair.generate();
const wallet = provider.wallet;
// Initialize Lever and Hand programs
const leverProgram = anchor.workspace.Lever as Program<Lever>;
const handProgram = anchor.workspace.Hand as Program<Hand>;
Here, we establish the testing setup by connecting to a local cluster, generating keypairs for data accounts, and initializing our Lever and Hand programs. Now, let's initialize the data account for the lever program:
it("Initialize the lever!", async () => {
// Initialize data account for the lever program
const tx = await leverProgram.methods
.new()
.accounts({ dataAccount: dataAccountLever.publicKey })
.signers([dataAccountLever])
.rpc();
console.log("Your transaction signature", tx);
// Fetch the state of the data account
const val = await leverProgram.methods
.get()
.accounts({ dataAccount: dataAccountLever.publicKey })
.view();
console.log("State:", val);
});
In this test case, we initialize a data account for the lever program and fetch its state.
it("Pull the lever!", async () => {
// Initialize data account for the hand program
// This is required by Solang, but the account is not used
const tx = await handProgram.methods
.new()
.accounts({ dataAccount: dataAccountHand.publicKey })
.signers([dataAccountHand])
.rpc();
console.log("Your transaction signature", tx);
// Call the pullLever instruction on the hand program, which invokes the lever program via CPI
const tx2 = await handProgram.methods
.pullLever(dataAccountLever.publicKey, "Chris")
.accounts({ dataAccount: dataAccountHand.publicKey })
.remainingAccounts([
{
pubkey: dataAccountLever.publicKey, // The lever program's data account, which stores the state
isWritable: true,
isSigner: false,
},
{
pubkey: leverProgram.programId, // The lever program's program ID
isWritable: false,
isSigner: false,
},
])
.rpc({ skipPreflight: true });
console.log("Your transaction signature", tx2);
// Fetch the state of the data account
const val = await leverProgram.methods
.get()
.accounts({ dataAccount: dataAccountLever.publicKey })
.view();
console.log("State:", val);
});
This test case focuses on the functionality of pulling the lever, where the hand program calls the lever program via CPI. We check the updated state post-CPI execution.
it("Pull it again!", async () => {
// Repeat lever pull with different arguments
const tx = await handProgram.methods
.pullLever(dataAccountLever.publicKey, "Ashley")
.accounts({ dataAccount: dataAccountHand.publicKey })
.remainingAccounts([
// Account configurations
])
.rpc({ skipPreflight: true });
console.log("Your transaction signature", tx);
// Fetch the state of the data account
const val = await leverProgram.methods
.get()
.accounts({ dataAccount: dataAccountLever.publicKey })
.view();
console.log("State:", val);
});
Here, we perform another lever pull with varying arguments to ensure the program responds correctly to different inputs.
In the next lesson, we will explore the process of building and deploying our Solana program to the blockchain, further advancing our understanding of Solana development.