Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unit Tests

A test declares a concrete scenario against one entity: optional initial state, message/system setup, one or more calls, and expect* assertions.

Syntax

test "fund changes state" for Escrow {
    let buyer = 0x0000000000000000000000000000000000000001
    let seller = 0x0000000000000000000000000000000000000002
    let arbiter = 0x0000000000000000000000000000000000000003
    call constructor(buyer, seller, arbiter, 1000, 9999)
    expect state { m_state: 0 }

    msg { sender: buyer }
    call fund()
    expect state { m_state: 1, m_funded: true }
}

Modifier order when skipping sender checks:

test "…" for <Entity> skip from with { … } { … }

skip from sits between the entity name and the optional with block.

Building blocks

ElementMeaning
test "name" for EntityDeclaration; entity must exist
skip fromDisable all from-clause sender checks in this test
with { field: value, … }Initial entity state (members only)
let name = expr / let name: T = exprBinding visible for the rest of the test; an untyped let takes its type from its uses (a literal used as a sender, address parameter, or map key is an address)
msg { sender: …, value: … } / sys { timestamp: …, … }Message / system context for the following calls
call route(args)Invoke a route (arity must match)
expect state { … }Assert members after a call
expect throw NAssert the call reverts with numeric code N
expect emit Event(args)Assert the next call emits Event(args) (place it before the call)
expect return EAssert return payload equals E (route must declare -> T)
expect return == ESame equality assertion as expect return E
expect <bool>Assert an arbitrary boolean after the call
expect effects […]Assert outgoing effects (backend-dependent; prefer state/return on EVM)

Plain tests have no ctx { … } block; that belongs to properties and invariants. On Lean, every expect after a call is kept and conjoined in the theorem goal.

test "withdraw emits" for Wallet with { m_balance: 10 } {
    let alice = 0x0000000000000000000000000000000000000001
    msg { sender: alice }
    expect emit Withdrawn(alice, 4)
    call withdraw(4)
    expect state { m_balance: 6 }
}

The event may be declared at program scope or inside the entity.

Lenses

Return values and member reads can be compared directly or through a field / index path:

expect return == (0, 5)
expect return != 0
expect return > amount
expect return.reserve_a > 0

expect m_balances[alice] == 900
expect m_state == Status::Funded

expect return E and expect return == E compare for equality. !=, <, <=, >, and >= (including on return.field) are boolean predicates, as is any other boolean expression:

call pay(bob, 30)
expect m_spent + m_balance == start
call left()
expect return > 0 && m_balance >= 70

expect state also takes field paths and map keys, and return values take tuple / length lenses; these run on both EVM and Lean:

expect state { m_pool.reserve_a: 1500 }
expect state { m_balances[alice]: 900 }
expect state { m_state: Status::Funded, m_opt: some(3) }
expect return.0 == 0
expect return.len == 3

On EVM, expect state reads members through their public Solidity getters. A Vec member (or a record or map holding a Vec / HashMap) has none, so expect state on it is rejected (T39); expose it through a view route and assert with expect return instead.

A bare member name is the post-state field after that call. Bind the starting value with with { m_balance: start } or a parameter. expect state { … } stays a map of equalities.

When the assertion does not start with the return keyword, the preceding call’s return value is the name result. That name is reserved in this expect: a parameter, let, or member also called result makes the predicate T38 if it mentions result. A non-boolean shape (tuple, record, array, empty collection) is also T38. expect before any call is still T5.

A whole-map literal (expect state { m_balances: { alice => 900 } }) checks each listed key on EVM, but does not yet build on Lean; for tests that must run on both targets, check maps key by key (expect m_balances[alice] == 900).

Multi-step tests

State threads automatically: output of one call is input to the next, with or without an intervening expect.

test "multi-step" for Counter with { m_count: 0 } {
    call increment(1)
    expect state { m_count: 1 }

    call increment(1)
    expect state { m_count: 2 }
}

skip from vs authentic senders

Routes with from Entity(…) verify msg.sender. For unit tests of business logic, skip from disables that check. For end-to-end sender checks on EVM, set msg { sender: … } to the address the from clause expects (often Entity.address(…)).

with vs context (brief)

  • with / state pins — entity members only (m_count: 0). A msg:: / sys:: key inside a test’s with is a parse error.
  • msg { … } / sys { … } steps — message and system context in plain tests (msg { sender: alice, value: 100 }, sys { timestamp: 100 }); they apply to the calls that follow.

Properties and invariants use a separate ctx { msg::sender: …, sys::now: … } block instead (T22 rejects context keys in their with / init). See Properties.