Skip to main content

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
Output includes:
  • 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)
      })
    );
  });
});

Test Coverage

Generate a coverage report:
npm run test:report -- --coverage
Output shows:
📊 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
Example output:
💰 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

Use beforeEach 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

Build docs developers (and LLMs) love