Get familiar with the anatomy of a GEN contract — the root component, the
ABI, and what push and deploy actually do — then run your first test.
Contracts: the root component
Both my_token_contracts.rs (cloned) and the generated
simple_token_contracts.rs have a root module:
/// Root component for XToken
pub mod root {
/* a lot of stuff here */
}
With a matching struct and impl:
/// Root implementation for XToken
pub struct XTokenRoot;
impl XTokenRoot {
/* functions here */
}
Two required functions on root
#[deploy]
pub async fn deploy() -> Result<Self> {
Ok(Self)
}
Called when the contract is deployed and the root component is installed.
Instantiate root members or store parameters that other components will need
at install time.
#[installation_request]
pub async fn installation_request(
&mut self,
_component_type: XTokenComponents,
) -> Result<()> {
Ok(())
}
Called when root receives a request to install a component. Enforce conditions
(e.g., max-installed) or pass parameters that the component needs.
Non-root components
pub mod component_1 {
pub struct Component1;
impl Component1 {
#[install]
pub async fn install(_installation_return_value: ()) -> Result<Self> {
Ok(Self)
}
}
}
Tests: ABI-driven typed clients
Both test files include:
#[generate_contract_client("[PATH]_token_modules.json")]
mod x_token_contract {}
This macro generates typed clients from the ABI — a JSON description of:
- What functions exist
- What parameters they take
- What they return
- How data is encoded/decoded
The module manifest and a per-component ABI
The module manifest:
{
"schema_version": "1.0",
"package_name": "my-token",
"contract_name": "my_token",
"modules": [
{ "name": "root", "component_type_index": 0,
"object_path": "my_token_0_root.o",
"abi_path": "my_token_0_root.abi.json" },
{ "name": "component_1", "component_type_index": 1,
"object_path": "my_token_1_component_1.o",
"abi_path": "my_token_1_component_1.abi.json" }
]
}
A per-component ABI:
{
"contract_name": "my_token",
"encoding": "Borsh",
"module_name": "component_1",
"methods": [
{
"name": "install",
"access_modifier": "runtime_only",
"params": [{ "name": "_installation_return_value",
"type": { "tuple": [] } }],
"returns": [],
"is_async": true
}
]
}
What’s in a test?
Set up context and accounts
let ctx = test_context.await;
let deployer_signer = ctx.new_signer();
let deployer_account = deployer_signer.account();
ctx.create_accounts(&[deployer_account]).await.unwrap_or_else(|e|
panic!("create_accounts failed: {e}{LOCAL_RUN_INSTRUCTIONS}")
);
Initialize the client
let gen_client = ctx.gen_client()
.unwrap_or_else(|e| panic!("gen client creation failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
The client lets you interact with entities on the blockchain — execution
spaces with storage that can install components onto themselves or other
entities.
Push the contract code
let push_id = simple_token_contract
::push_contract(deployer_account, tokio::fs::read).await
.unwrap_or_else(|e| panic!("contract push payload build failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
.execute(&gen_client, &deployer_signer).await
.unwrap_or_else(|e| panic!("contract push failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
An external push call lands on default_entity(0), which copies the
contract code into the entity’s storage.
1. The entity (default_entity(0)) receives the external push call.
2. The contract code is copied into the entity’s storage.
Deploy the code (install the root component)
let root = simple_token_contract
::deploy_contract(deployer_account, push_id)
.unwrap_or_else(|e| panic!("contract deploy payload build failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
.execute(&gen_client, &deployer_signer).await
.unwrap_or_else(|e| panic!("contract deploy failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
let _root_contract = root.gvm_contract();
1. The external call asks the entity to create an instance of the code that was pushed using pushId.
2. The entity deploys the contract by installing the root component (in this case, onto itself).
Install a component
let _component_1 = simple_token_contract::install_component_1(
deployer_account,
_root_contract,
)
.expect("component_1 installation payload build should succeed")
.execute(&gen_client, &deployer_signer).await
.expect("component_1 installation should succeed");
1. The external call requests installation of component_1 via the deployed root.
2. Root forwards the installation request to the target entity.
3. The component is installed and a typed handle is returned.
It’s showtime
Build
cd my-token
gen genie build my-token
cd ../simple-token
gen genie build simple-token
Test
cargo test -p my-token-tests
# if gen is not on your PATH:
GEN_CLI_PATH=~/bin/gen cargo test -p my-token-tests
# full backtrace:
RUST_BACKTRACE=1 cargo test -p my-token-tests
# verbose (your println!s):
cargo test -p my-token-tests -- --nocapture
# repeatable scenario (same entity IDs):
TEST_SEED=12345 cargo test -p my-token-tests
build.rs detects changes in your contract source files, runs gen genie build automatically, and regenerates the ABI files used by
#[generate_contract_client]. That means you can edit code and just run
cargo test — but the first test will take a few minutes as it builds
the contract and all dependencies.
Choose a test workflow
The default cargo test runs each test against a fresh validator subprocess,
which is fine but slow. The IDE and helper scripts the scaffolding ships with
keep a single validator running across runs:
| Environment | Recommended workflow | Notes |
|---|
| VS Code / Cursor | Click Run Test or Debug on any #[test], or use the Run and Debug panel entries | The .vscode/bin/cargo shim starts a local validator automatically via scripts/ensure-local-validator.sh |
| Terminal | ./scripts/test-with-local-validator.sh -p my-token-tests | Same as the IDE flow: starts a background validator, sets GEN_VALIDATOR_MODE=external, runs cargo test |
| Docker (test-container) | GEN_VALIDATOR_MODE=test-container cargo test -p my-token-tests | Requires the gen-cli:latest image; install.sh builds it automatically when Docker is reachable |
| Manual | Run gen service validator local --embedded-config in one terminal, then GEN_VALIDATOR_MODE=external cargo test -p my-token-tests in another | Full control; no helper scripts needed |
For the full reference on validator modes and test fixtures, see
Contract test tools.
Checkpoint — cargo test -p my-token-tests passes.