Testing Guide

Testing Strategies

Comprehensive Testing for Smart Contracts

01

Unit Testing Comparison

Daml Script Testing Philosophy

  • Scripts run against an in-memory ledger
  • Test multi-party workflows naturally
  • Authorization is verified automatically
  • Time travel for time-based logic

CosmWasm Testing Philosophy

  • Mock dependencies for isolation
  • Test state transitions explicitly
  • Verify error conditions manually
  • Use multi-test for integration
Daml Script

Multi-Party Scenario Testing

module Test.Asset where

import Daml.Script
import Main

-- Test asset creation and transfer
testAssetLifecycle : Script ()
testAssetLifecycle = script do
  -- Setup: Allocate parties
  issuer <- allocateParty "GoldMint"
  alice <- allocateParty "Alice"
  bob <- allocateParty "Bob"

  -- Act: Create asset with issuer
  assetCid <- submit issuer do
    createCmd Asset with
      issuer = issuer
      owner = issuer
      description = "1oz Gold Bar"
      quantity = 100.0

  -- Transfer to Alice (issuer must transfer first)
  assetCid <- submit issuer do
    exerciseCmd assetCid Transfer with
      newOwner = alice

  -- Alice transfers to Bob
  assetCid <- submit alice do
    exerciseCmd assetCid Transfer with
      newOwner = bob

  -- Assert: Verify final state
  Some asset <- queryContractId bob assetCid
  asset.owner === bob
  asset.quantity === 100.0

-- Test authorization failure
testUnauthorizedTransfer : Script ()
testUnauthorizedTransfer = script do
  issuer <- allocateParty "Issuer"
  alice <- allocateParty "Alice"
  mallory <- allocateParty "Mallory"

  assetCid <- submit issuer do
    createCmd Asset with
      issuer, owner = issuer
      description = "Diamond"
      quantity = 1.0

  assetCid <- submit issuer do
    exerciseCmd assetCid Transfer with newOwner = alice

  -- This should fail - Mallory can't transfer Alice's asset
  submitMustFail mallory do
    exerciseCmd assetCid Transfer with newOwner = mallory

-- Test with time
testTimeBasedVesting : Script ()
testTimeBasedVesting = script do
  employee <- allocateParty "Employee"
  company <- allocateParty "Company"

  vestingCid <- submit company do
    createCmd VestingSchedule with
      company, employee
      vestingDate = datetime 2025 Jun 1 0 0 0

  -- Before vesting date - should fail
  setTime (datetime 2025 May 1 0 0 0)
  submitMustFail employee do
    exerciseCmd vestingCid Claim

  -- After vesting date - should succeed
  setTime (datetime 2025 Jul 1 0 0 0)
  submit employee do
    exerciseCmd vestingCid Claim
Rust

Mock-Based Unit Testing

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{
        mock_dependencies, mock_env, mock_info,
        MockApi, MockQuerier, MockStorage,
    };
    use cosmwasm_std::{coins, Addr, Timestamp};

    fn setup_contract() -> OwnedDeps {
        let mut deps = mock_dependencies();
        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            initial_supply: Uint128::new(1000000),
        };
        let info = mock_info("creator", &coins(1000, "uatom"));
        instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
        deps
    }

    #[test]
    fn test_transfer_success() {
        let mut deps = setup_contract();

        // Mint tokens to alice
        let info = mock_info("admin", &[]);
        let msg = ExecuteMsg::Mint {
            recipient: "alice".to_string(),
            amount: Uint128::new(100),
        };
        execute(deps.as_mut(), mock_env(), info, msg).unwrap();

        // Alice transfers to bob
        let info = mock_info("alice", &[]);
        let msg = ExecuteMsg::Transfer {
            recipient: "bob".to_string(),
            amount: Uint128::new(50),
        };
        let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();

        // Verify event attributes
        assert_eq!(res.attributes[0].value, "transfer");

        // Query balances
        let alice_balance: BalanceResponse = from_json(
            query(
                deps.as_ref(),
                mock_env(),
                QueryMsg::Balance { address: "alice".to_string() },
            ).unwrap()
        ).unwrap();
        assert_eq!(alice_balance.balance, Uint128::new(50));

        let bob_balance: BalanceResponse = from_json(
            query(
                deps.as_ref(),
                mock_env(),
                QueryMsg::Balance { address: "bob".to_string() },
            ).unwrap()
        ).unwrap();
        assert_eq!(bob_balance.balance, Uint128::new(50));
    }

    #[test]
    fn test_unauthorized_transfer() {
        let mut deps = setup_contract();

        // Try to transfer without balance - should error
        let info = mock_info("mallory", &[]);
        let msg = ExecuteMsg::Transfer {
            recipient: "thief".to_string(),
            amount: Uint128::new(100),
        };
        let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
        assert!(matches!(err, ContractError::InsufficientFunds { .. }));
    }

    #[test]
    fn test_time_based_vesting() {
        let mut deps = setup_contract();

        // Create vesting
        let info = mock_info("company", &coins(1000, "utoken"));
        let vest_time = Timestamp::from_seconds(1735689600); // 2025-01-01
        let msg = ExecuteMsg::CreateVesting {
            beneficiary: "employee".to_string(),
            vest_time,
        };
        execute(deps.as_mut(), mock_env(), info, msg).unwrap();

        // Before vest time - should fail
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(1704067200); // 2024-01-01
        let info = mock_info("employee", &[]);
        let msg = ExecuteMsg::ClaimVesting {};
        let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
        assert!(matches!(err, ContractError::VestingNotMature {}));

        // After vest time - should succeed
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(1767225600); // 2026-01-01
        let info = mock_info("employee", &[]);
        let msg = ExecuteMsg::ClaimVesting {};
        execute(deps.as_mut(), env, info, msg).unwrap();
    }
}
02

Integration Testing

Daml Script

Full Workflow Integration Test

-- Integration test: Complete DVP settlement workflow
testDVPWorkflow : Script ()
testDVPWorkflow = script do
  -- Setup market participants
  exchange <- allocateParty "Exchange"
  bank <- allocateParty "CustodianBank"
  buyer <- allocateParty "HedgeFund"
  seller <- allocateParty "AssetManager"

  -- Create initial positions
  -- Seller has securities
  securityCid <- submit exchange do
    createCmd Security with
      exchange
      holder = seller
      cusip = "912828ZT6"  -- Treasury bond
      quantity = 1000000

  -- Buyer has cash
  cashCid <- submit bank do
    createCmd CashAccount with
      bank
      owner = buyer
      currency = "USD"
      balance = 1050000.0

  -- Create DVP instruction
  dvpCid <- submit buyer do
    createCmd DVPInstruction with
      buyer, seller, exchange, bank
      securityId = "912828ZT6"
      quantity = 1000000
      price = 1.02  -- 102% of face value

  -- Seller accepts
  dvpCid <- submit seller do
    exerciseCmd dvpCid AcceptDVP

  -- Settlement - atomic delivery vs payment
  (newSecCid, newCashCid) <- submitMulti [buyer, seller] [] do
    exerciseCmd dvpCid Settle

  -- Verify: Securities transferred
  Some security <- queryContractId buyer newSecCid
  security.holder === buyer

  -- Verify: Payment made
  Some cash <- queryContractId seller newCashCid
  cash.balance === 1020000.0  -- 1M * 1.02
Rust

Multi-Contract Integration Test

use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor};

fn mock_app() -> App {
    AppBuilder::new().build(|router, _, storage| {
        router
            .bank
            .init_balance(
                storage,
                &Addr::unchecked("buyer"),
                coins(1_050_000, "uusd"),
            )
            .unwrap();
    })
}

#[test]
fn test_dvp_workflow() {
    let mut app = mock_app();

    // Store contracts
    let token_code = ContractWrapper::new(
        token::execute, token::instantiate, token::query,
    );
    let token_code_id = app.store_code(Box::new(token_code));

    let escrow_code = ContractWrapper::new(
        escrow::execute, escrow::instantiate, escrow::query,
    );
    let escrow_code_id = app.store_code(Box::new(escrow_code));

    // Instantiate security token
    let security_addr = app
        .instantiate_contract(
            token_code_id,
            Addr::unchecked("exchange"),
            &TokenInstantiateMsg {
                name: "Treasury".to_string(),
                symbol: "TBOND".to_string(),
                decimals: 0,
                initial_balances: vec![Cw20Coin {
                    address: "seller".to_string(),
                    amount: Uint128::new(1_000_000),
                }],
            },
            &[],
            "security_token",
            None,
        )
        .unwrap();

    // Instantiate escrow for DVP
    let escrow_addr = app
        .instantiate_contract(
            escrow_code_id,
            Addr::unchecked("exchange"),
            &EscrowInstantiateMsg {
                security_token: security_addr.to_string(),
                price_denom: "uusd".to_string(),
                price_per_unit: Uint128::new(1_020_000),
            },
            &[],
            "dvp_escrow",
            None,
        )
        .unwrap();

    // Seller deposits securities
    app.execute_contract(
        Addr::unchecked("seller"),
        security_addr.clone(),
        &Cw20ExecuteMsg::Send {
            contract: escrow_addr.to_string(),
            amount: Uint128::new(1_000_000),
            msg: to_json_binary(&EscrowReceiveMsg::DepositSecurity {}).unwrap(),
        },
        &[],
    ).unwrap();

    // Buyer deposits payment and triggers settlement
    app.execute_contract(
        Addr::unchecked("buyer"),
        escrow_addr.clone(),
        &EscrowExecuteMsg::Settle {},
        &coins(1_020_000, "uusd"),
    ).unwrap();

    // Verify buyer received securities
    let balance: cw20::BalanceResponse = app
        .wrap()
        .query_wasm_smart(
            security_addr,
            &Cw20QueryMsg::Balance { address: "buyer".to_string() },
        )
        .unwrap();
    assert_eq!(balance.balance, Uint128::new(1_000_000));

    // Verify seller received payment
    let seller_balance = app.wrap().query_balance("seller", "uusd").unwrap();
    assert_eq!(seller_balance.amount, Uint128::new(1_020_000));
}
03

Property-Based Testing

Property-based testing verifies invariants hold across many random inputs, catching edge cases traditional tests miss.

Daml

Invariant Testing with QuickCheck-style

-- Property: Total supply is conserved across transfers
propConserveSupply : [Party] -> Script Bool
propConserveSupply parties = script do
  issuer <- allocateParty "Issuer"

  -- Create initial supply
  let totalSupply = 1000000.0
  assetCid <- submit issuer do
    createCmd Asset with
      issuer, owner = issuer
      description = "Token"
      quantity = totalSupply

  -- Perform random transfers
  finalAssets <- foldlA (performRandomTransfer issuer) [assetCid] parties

  -- Sum all quantities
  quantities <- forA finalAssets \cid -> do
    Some asset <- queryContractId issuer cid
    pure asset.quantity

  -- Property: Total equals initial supply
  pure (sum quantities == totalSupply)

-- Property: No double-spend possible
propNoDoubleSpend : Script ()
propNoDoubleSpend = script do
  alice <- allocateParty "Alice"
  bob <- allocateParty "Bob"
  carol <- allocateParty "Carol"
  issuer <- allocateParty "Issuer"

  assetCid <- submit issuer do
    createCmd Asset with
      issuer, owner = alice
      description = "NFT"
      quantity = 1.0

  -- First transfer succeeds
  newCid <- submit alice do
    exerciseCmd assetCid Transfer with newOwner = bob

  -- Attempting to use archived contract fails
  submitMustFail alice do
    exerciseCmd assetCid Transfer with newOwner = carol

-- Property: Only owner can transfer
propOwnerControl : Party -> Party -> Script ()
propOwnerControl owner attacker = script do
  issuer <- allocateParty "Issuer"

  assetCid <- submit issuer do
    createCmd Asset with
      issuer, owner
      description = "Secured"
      quantity = 100.0

  -- Attacker cannot transfer
  submitMustFail attacker do
    exerciseCmd assetCid Transfer with newOwner = attacker
Rust

Proptest Integration

use proptest::prelude::*;

proptest! {
    // Property: Total supply is conserved
    #[test]
    fn prop_conserve_supply(
        initial_supply in 1u128..1_000_000_000u128,
        transfers in prop::collection::vec(
            (0usize..10, 0usize..10, 1u128..1000u128),
            0..50
        )
    ) {
        let mut deps = mock_dependencies();

        // Initialize with supply
        let msg = InstantiateMsg {
            initial_supply: Uint128::new(initial_supply),
        };
        instantiate(deps.as_mut(), mock_env(), mock_info("admin", &[]), msg).unwrap();

        // Distribute to accounts
        let accounts: Vec = (0..10).map(|i| format!("user{}", i)).collect();
        let per_account = initial_supply / 10;

        for acc in &accounts {
            let _ = execute(
                deps.as_mut(),
                mock_env(),
                mock_info("admin", &[]),
                ExecuteMsg::Mint {
                    recipient: acc.clone(),
                    amount: Uint128::new(per_account),
                },
            );
        }

        // Perform transfers (may fail, that's ok)
        for (from_idx, to_idx, amount) in transfers {
            let from = &accounts[from_idx % accounts.len()];
            let to = &accounts[to_idx % accounts.len()];
            let _ = execute(
                deps.as_mut(),
                mock_env(),
                mock_info(from, &[]),
                ExecuteMsg::Transfer {
                    recipient: to.clone(),
                    amount: Uint128::new(amount),
                },
            );
        }

        // Verify: Sum of all balances equals total supply
        let total: u128 = accounts.iter().map(|acc| {
            let res: BalanceResponse = from_json(
                query(deps.as_ref(), mock_env(), QueryMsg::Balance {
                    address: acc.clone()
                }).unwrap()
            ).unwrap();
            res.balance.u128()
        }).sum();

        // Admin balance + distributed
        let admin_bal: BalanceResponse = from_json(
            query(deps.as_ref(), mock_env(), QueryMsg::Balance {
                address: "admin".to_string()
            }).unwrap()
        ).unwrap();

        prop_assert_eq!(total + admin_bal.balance.u128(), initial_supply);
    }

    // Property: Unauthorized transfers always fail
    #[test]
    fn prop_no_unauthorized_transfer(
        owner in "[a-z]{5,10}",
        attacker in "[a-z]{5,10}",
        amount in 1u128..1000u128,
    ) {
        prop_assume!(owner != attacker);

        let mut deps = mock_dependencies();
        instantiate(
            deps.as_mut(),
            mock_env(),
            mock_info("admin", &[]),
            InstantiateMsg { initial_supply: Uint128::new(1000) },
        ).unwrap();

        // Give owner tokens
        execute(
            deps.as_mut(),
            mock_env(),
            mock_info("admin", &[]),
            ExecuteMsg::Mint {
                recipient: owner.clone(),
                amount: Uint128::new(amount),
            },
        ).unwrap();

        // Attacker cannot transfer owner's tokens
        let result = execute(
            deps.as_mut(),
            mock_env(),
            mock_info(&attacker, &[]),
            ExecuteMsg::TransferFrom {
                owner: owner.clone(),
                recipient: attacker.clone(),
                amount: Uint128::new(amount),
            },
        );

        prop_assert!(result.is_err());
    }
}
04

CI/CD Pipelines

YAML

Daml GitHub Actions

name: Daml CI

on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Daml
        uses: digital-asset/setup-daml@v2
        with:
          daml-version: 2.8.0

      - name: Build
        run: daml build

      - name: Run Tests
        run: daml test --junit-report test-results.xml

      - name: Type Check
        run: daml damlc lint

      - name: Generate Docs
        run: daml damlc docs --output docs

      - name: Upload Test Results
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: test-results.xml

  integration-test:
    needs: build-and-test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Daml
        uses: digital-asset/setup-daml@v2

      - name: Start Sandbox
        run: |
          daml sandbox --port 6865 &
          sleep 10

      - name: Run Integration Scripts
        run: |
          daml script \
            --dar .daml/dist/*.dar \
            --script-name Test.Integration:fullWorkflow \
            --ledger-host localhost \
            --ledger-port 6865

      - name: Shutdown
        run: pkill -f sandbox || true
YAML

CosmWasm GitHub Actions

name: CosmWasm CI

on: [push, pull_request]

env:
  CARGO_TERM_COLOR: always

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
          components: clippy, rustfmt

      - name: Cache
        uses: Swatinem/rust-cache@v2

      - name: Format Check
        run: cargo fmt --all -- --check

      - name: Clippy
        run: cargo clippy --all-targets -- -D warnings

      - name: Unit Tests
        run: cargo test --lib

      - name: Integration Tests
        run: cargo test --test integration

      - name: Build WASM
        run: |
          cargo build --release --target wasm32-unknown-unknown
          mkdir -p artifacts
          cp target/wasm32-unknown-unknown/release/*.wasm artifacts/

      - name: Check WASM Size
        run: |
          for wasm in artifacts/*.wasm; do
            size=$(stat -f%z "$wasm" 2>/dev/null || stat -c%s "$wasm")
            if [ $size -gt 800000 ]; then
              echo "WASM too large: $wasm ($size bytes)"
              exit 1
            fi
          done

  optimize:
    needs: test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Optimize WASM
        run: |
          docker run --rm -v "$(pwd)":/code \
            --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
            cosmwasm/optimizer:0.15.0

      - name: Upload Artifacts
        uses: actions/upload-artifact@v3
        with:
          name: optimized-contracts
          path: artifacts/*.wasm
05

Testing Comparison Matrix

Testing Aspect Daml CosmWasm
Unit Test Framework Daml Script (built-in) Rust #[test] + cosmwasm-std::testing
Multi-Party Testing Native (allocateParty, submit) Manual (mock_info per call)
Authorization Testing submitMustFail (built-in) assert!(result.is_err())
Time Manipulation setTime function Modify mock_env().block.time
Integration Testing Same scripts against sandbox cw-multi-test crate
Property Testing QuickCheck-style scripts proptest crate
Coverage Tools daml test --coverage cargo tarpaulin / llvm-cov
Mocking Not needed (real ledger) MockQuerier, mock_dependencies