Overview
Staxiq smart contracts are tested using Vitest with the Clarinet SDK, providing a fast, type-safe testing environment for Clarity contracts.Unit Tests
Test individual contract functions in isolation
Integration Tests
Test complete user workflows and interactions
Cost Analysis
Measure gas costs for contract operations
Coverage Reports
Track which contract paths are tested
Test Setup
Dependencies
The test environment uses:package.json
{
"scripts": {
"test": "vitest run",
"test:report": "vitest run -- --coverage --costs",
"test:watch": "chokidar \"tests/**/*.ts\" \"contracts/**/*.clar\" -c \"npm run test:report\""
},
"dependencies": {
"@stacks/clarinet-sdk": "^3.9.0",
"@stacks/transactions": "^7.2.0",
"@types/node": "^24.4.0",
"chokidar-cli": "^3.0.0",
"vitest": "^4.0.7",
"vitest-environment-clarinet": "^3.0.0"
}
}
Vitest Configuration
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'clarinet',
singleThread: true,
},
});
The
clarinet environment provides a simulated Stacks blockchain for testing.Running Tests
Basic Test Run
Run all tests once:npm test
Detailed Test Report
Run with coverage and cost analysis:npm run test:report
- Test results
- Contract execution costs
- Code coverage percentage
- Gas usage per function
Watch Mode
Auto-run tests when files change:npm run test:watch
Use watch mode during development for instant feedback on code changes.
Writing Tests
Test Structure
The test file structure:tests/staxiq-user-profile.test.ts
import { describe, expect, it } from "vitest";
const accounts = simnet.getAccounts();
const address1 = accounts.get("wallet_1")!;
describe("staxiq-user-profile", () => {
it("ensures simnet is well initialised", () => {
expect(simnet.blockHeight).toBeDefined();
});
});
Example Tests
Here are comprehensive tests for the user profile contract:import { describe, expect, it, beforeEach } from "vitest";
import { Cl } from "@stacks/transactions";
const accounts = simnet.getAccounts();
const deployer = accounts.get("deployer")!;
const user1 = accounts.get("wallet_1")!;
const user2 = accounts.get("wallet_2")!;
describe("set-risk-profile", () => {
it("should set Conservative risk profile", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(1)],
user1
);
expect(result).toBeOk(Cl.uint(1));
});
it("should set Balanced risk profile", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
expect(result).toBeOk(Cl.uint(2));
});
it("should set Aggressive risk profile", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(3)],
user1
);
expect(result).toBeOk(Cl.uint(3));
});
it("should reject invalid risk level (0)", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(0)],
user1
);
expect(result).toBeErr(Cl.uint(400)); // ERR-INVALID-RISK
});
it("should reject invalid risk level (4)", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(4)],
user1
);
expect(result).toBeErr(Cl.uint(400));
});
it("should update existing profile", () => {
// Set initial profile
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(1)],
user1
);
// Update to different risk level
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(3)],
user1
);
expect(result).toBeOk(Cl.uint(3));
// Verify profile updated
const { result: profile } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-user-profile",
[Cl.principal(user1)],
user1
);
expect(profile).toBeOk(
Cl.tuple({
'risk-level': Cl.uint(3),
'created-at': Cl.uint(expect.any(Number)),
'updated-at': Cl.uint(expect.any(Number)),
'strategy-count': Cl.uint(0)
})
);
});
});
describe("save-strategy", () => {
beforeEach(() => {
// Ensure user has a profile before each test
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
});
it("should save strategy successfully", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii("a1b2c3d4e5f6789012345678901234567890123456789012345678901234"),
Cl.stringAscii("ALEX")
],
user1
);
expect(result).toBeOk(Cl.uint(1)); // First strategy ID
});
it("should increment strategy ID for multiple strategies", () => {
// Save first strategy
const { result: result1 } = simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii("hash1111111111111111111111111111111111111111111111111111111111"),
Cl.stringAscii("ALEX")
],
user1
);
expect(result1).toBeOk(Cl.uint(1));
// Save second strategy
const { result: result2 } = simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii("hash2222222222222222222222222222222222222222222222222222222222"),
Cl.stringAscii("Velar")
],
user1
);
expect(result2).toBeOk(Cl.uint(2));
// Save third strategy
const { result: result3 } = simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii("hash3333333333333333333333333333333333333333333333333333333333"),
Cl.stringAscii("StackSwap")
],
user1
);
expect(result3).toBeOk(Cl.uint(3));
});
it("should fail if user has no profile", () => {
const { result } = simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii("a1b2c3d4e5f6789012345678901234567890123456789012345678901234"),
Cl.stringAscii("ALEX")
],
user2 // user2 has no profile
);
expect(result).toBeErr(Cl.uint(404)); // ERR-NOT-FOUND
});
it("should store strategy with correct details", () => {
// Save strategy
simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii("abc123def456"),
Cl.stringAscii("ALEX")
],
user1
);
// Retrieve strategy
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-strategy",
[Cl.principal(user1), Cl.uint(1)],
user1
);
expect(result).toBeOk(
Cl.tuple({
'risk-level': Cl.uint(2),
'strategy-hash': Cl.stringAscii("abc123def456"),
'protocol': Cl.stringAscii("ALEX"),
'timestamp': Cl.uint(expect.any(Number))
})
);
});
});
describe("get-user-profile", () => {
it("should return profile for user with profile", () => {
// Create profile
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
// Get profile
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-user-profile",
[Cl.principal(user1)],
user1
);
expect(result).toBeOk(
Cl.tuple({
'risk-level': Cl.uint(2),
'created-at': Cl.uint(expect.any(Number)),
'updated-at': Cl.uint(expect.any(Number)),
'strategy-count': Cl.uint(0)
})
);
});
it("should return ERR-NOT-FOUND for user without profile", () => {
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-user-profile",
[Cl.principal(user2)],
user2
);
expect(result).toBeErr(Cl.uint(404));
});
});
describe("has-profile", () => {
it("should return true for user with profile", () => {
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(1)],
user1
);
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"has-profile",
[Cl.principal(user1)],
user1
);
expect(result).toBeBool(true);
});
it("should return false for user without profile", () => {
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"has-profile",
[Cl.principal(user2)],
user2
);
expect(result).toBeBool(false);
});
});
describe("get-strategy-count", () => {
it("should return 0 for user with no strategies", () => {
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-strategy-count",
[Cl.principal(user1)],
user1
);
expect(result).toBeOk(Cl.uint(0));
});
it("should return correct count after saving strategies", () => {
// Setup
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
// Save 3 strategies
for (let i = 1; i <= 3; i++) {
simnet.callPublicFn(
"staxiq-user-profile",
"save-strategy",
[
Cl.stringAscii(`hash${i}`.padEnd(64, '0')),
Cl.stringAscii("ALEX")
],
user1
);
}
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-strategy-count",
[Cl.principal(user1)],
user1
);
expect(result).toBeOk(Cl.uint(3));
});
});
describe("get-risk-label", () => {
it("should return 'Conservative' for risk level 1", () => {
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(1)],
user1
);
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-risk-label",
[Cl.principal(user1)],
user1
);
expect(result).toBeOk(Cl.stringAscii("Conservative"));
});
it("should return 'Balanced' for risk level 2", () => {
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-risk-label",
[Cl.principal(user1)],
user1
);
expect(result).toBeOk(Cl.stringAscii("Balanced"));
});
it("should return 'Aggressive' for risk level 3", () => {
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(3)],
user1
);
const { result } = simnet.callReadOnlyFn(
"staxiq-user-profile",
"get-risk-label",
[Cl.principal(user1)],
user1
);
expect(result).toBeOk(Cl.stringAscii("Aggressive"));
});
});
Test Coverage
Generate a coverage report:npm run test:report -- --coverage
📊 Coverage Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
File | Lines | Branches
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
staxiq-user-profile.clar | 98% | 95%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Aim for >90% coverage to ensure contract reliability.
Gas Cost Analysis
Vitest can report execution costs:npm run test:report -- --costs
💰 Gas Cost Analysis
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function | Execute | Read | Write
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
set-risk-profile | 5,200 | 150 | 300
save-strategy | 7,500 | 200 | 450
get-user-profile | 1,000 | 150 | 0
get-strategy | 1,200 | 180 | 0
get-strategy-count | 800 | 100 | 0
has-profile | 600 | 100 | 0
get-risk-label | 1,100 | 150 | 0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lower costs mean cheaper transactions for users. Optimize expensive functions if possible.
Best Practices
1. Test All Paths
Test both success and failure cases:// ✅ Good - tests both cases
it("should accept valid risk level", () => { /* ... */ });
it("should reject invalid risk level", () => { /* ... */ });
// ❌ Bad - only tests success
it("should set risk profile", () => { /* ... */ });
2. Use Descriptive Test Names
// ✅ Good
it("should return ERR-NOT-FOUND when user has no profile", () => { /* ... */ });
// ❌ Bad
it("test get profile", () => { /* ... */ });
3. Setup and Teardown
UsebeforeEach for common setup:
beforeEach(() => {
// Reset state or setup common conditions
simnet.callPublicFn(
"staxiq-user-profile",
"set-risk-profile",
[Cl.uint(2)],
user1
);
});
4. Test Edge Cases
- Boundary values (0, max uint)
- Empty strings
- Multiple users simultaneously
- Sequential operations
Continuous Integration
Add tests to your CI/CD pipeline:.github/workflows/test.yml
name: Contract Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm install
- run: npm test
- run: npm run test:report
Debugging Tests
Log Contract State
const { result } = simnet.callPublicFn(/* ... */);
console.log('Result:', result);
console.log('Block height:', simnet.blockHeight);
console.log('Accounts:', simnet.getAccounts());
Inspect Transaction Events
const { events } = simnet.callPublicFn(/* ... */);
console.log('Events:', events);
Next Steps
Deployment
Deploy tested contracts to testnet or mainnet
Integration
Integrate contracts into your frontend
Vitest Docs
Learn more about Vitest testing framework
Clarinet SDK
Explore Clarinet SDK documentation