Testing smart contracts is KEY when building on Bitcoin L2s like Stacks. You'll save a lot of time just by running your test before deploying to Testnet. In this post, I’ll walk through how I tested the SIP-009 compliant minter.clar
contract for the 4V4 project — a 3D NFT Marketplace where you can mint and trade avatars with satoshis.
🎯 What We Test
The minter.clar
contract enables:
- Whitelist-based minting
- Public minting
- NFT transfers
- Metadata URI management
- Royalty info for marketplaces
We built tests using @hirosystems/clarinet-sdk
and vitest
in TypeScript.
🛠️ Test Setup
We used initSimnet()
to spin up a temporary Clarinet devnet (simnet) for each test case. This gives us access to:
- A fresh blockchain state
- Wallet accounts
- Simulated block mining
beforeEach(async () => {
simnet = await initSimnet();
accounts = simnet.getAccounts();
});
✅ Test Cases
1. Add to Whitelist & Mint
We first confirmed an admin can whitelist a user and that user can mint within allowance:
await simnet.mineBlock([
tx.callPublicFn("minter", "add-to-whitelist", [
Cl.standardPrincipal(wallet1), Cl.uint(2)
], deployer),
]);
await simnet.mineBlock([
tx.callPublicFn("minter", "mint-whitelist", [
Cl.standardPrincipal(wallet1)
], wallet1),
]);
Then verified ownership:
const read = await simnet.callReadOnlyFn("minter", "get-owner", [Cl.uint(1)], wallet1);
expect(read.result).toEqual(Cl.ok(Cl.some(Cl.standardPrincipal(wallet1))));
2. Public Mint
Ensures anyone can mint after the whitelist phase:
await simnet.mineBlock([
tx.callPublicFn("minter", "mint-public", [], wallet2),
]);
3. Whitelist Overflow
We tested the whitelist limit to ensure a user cannot mint more than allowed:
expect(block[0].result.type).toBe(ClarityType.ResponseErr);
expect(block[0].result.value.value).toBe(403n);
4. Royalty Info
For marketplaces to calculate royalties dynamically:
const read = await simnet.callReadOnlyFn("minter", "get-royalty-info", [Cl.uint(1000)], deployer);
const result = read.result.value.data;
expect(result["amount"].value).toBe(50n); // 5% of 1000
expect(result["recipient"].value).toBe(deployer);
5. Setting & Reading Token URI
This test gave us the most headaches but was totally worth it. We validated that a user can set the token URI after minting and then retrieve it:
const uri = Cl.stringAscii("ipfs://avatar-0004");
await simnet.mineBlock([
tx.callPublicFn("minter", "set-token-uri", [Cl.uint(1), uri], wallet4),
]);
const read = await simnet.callReadOnlyFn("minter", "get-token-uri", [Cl.uint(1)], wallet4);
const optional = read.result.value;
expect(optional.type).toBe(ClarityType.OptionalSome);
const stringVal = optional.value;
expect(stringVal.type).toBe(ClarityType.StringASCII);
expect(stringVal.data).toBe("ipfs://avatar-0004");
Read the full test suite here.
🚀 Final Thoughts
This was a hot 🔥 session — I iterated FOR DAYS until all 5 tests passed and this now ensures a:
- Full confidence in the 3D NFT minting flow
- Robust logic for access control and metadata
- A reusable structure to test further functions
TIP: Ensure to check docs about clarinet-sdk and watch this video: youtube.com/watch?v=H-NKxafz7YU
Next up: we’ll deploy the contract to Testnet.