Skip to main content

How to Test a Contract

Use a test runner such as Mocha. Start with local BVM verification, then move outward to provider-backed flows. The example below covers both a stateless P2PKH unlock and a stateful Counter transition. The usual test shape is:

  1. Assert compile-time artifacts and pure state transitions.
  2. Use bound.methods.<name>.verify(...) for a fast local BVM check with a synthetic spend context.
  3. Use createMemoryProvider() for offline deploy/spend integration tests.

verify(...) does not need a provider, wallet UTXO set, or broadcast. It builds the same unlocking path locally and runs the compiled script through the BVM. Use MemoryProvider after that when you want to test deployment, UTXO mutation, successor instances, wallet funding, and fee/change behavior.

Mocha Test File

import { expect } from 'chai';
import {
P2PKH,
TypeTag,
add,
contract,
createMemoryProvider,
createSigner,
gt,
lit,
method,
} from '@opcat-labs/lambit';

const Counter = contract(
'Counter',
{},
{ count: TypeTag.Int },
({ state }) => ({
increase: method({}, () => {
const nextCount = add(state.count, lit(1n));
return {
next: { count: nextCount },
check: gt(nextCount, state.count),
};
}),
}),
);

describe('stateless P2PKH', () => {
it('verifies the unlock method locally with the BVM', async () => {
const signer = createSigner();
const addr = await signer.getPublicKeyHash();
const pubkey = await signer.getPublicKey();
const p2pkh = P2PKH({ addr });

let verification: Promise<void> | undefined;
expect(() => {
verification = p2pkh.methods.unlock.verify(
{},
{ pubkey },
{
context: {
satoshis: 1_000n,
txid: '22'.repeat(32),
vout: 0,
},
signer,
invoke: (psbt) => ({
sig: psbt.getSig(0, { address: addr }),
}),
},
);
}).not.to.throw();
await verification;
});

it('spends under MemoryProvider after the method verifies', async () => {
const provider = createMemoryProvider();
const signer = createSigner();
const addr = await signer.getPublicKeyHash();
const pubkey = await signer.getPublicKey();
const p2pkh = P2PKH({ addr });

const deployed = await p2pkh.deploy({ provider, satoshis: 1_000n });
let spend: Promise<unknown> | undefined;
expect(() => {
spend = deployed.methods.unlock.call(
{ pubkey },
{
provider,
signer,
invoke: (psbt) => ({
sig: psbt.getSig(0, { address: addr }),
}),
},
);
}).not.to.throw();
await spend;
});
});

describe('stateful Counter', () => {
it('checks the pure transition and verifies the stateful spend locally', async () => {
const counter = Counter();
const state = { count: 0n };
const nextState = counter.methods.increase.next(state);

expect(nextState).to.deep.equal({ count: 1n });

let verification: Promise<void> | undefined;
expect(() => {
verification = counter.methods.increase.verify(
state,
{
context: {
satoshis: 1_000n,
nextState,
},
},
);
}).not.to.throw();
await verification;
});

it('spends a stateful transition under MemoryProvider', async () => {
const provider = createMemoryProvider();
const counter = Counter();
const state = { count: 0n };
const nextState = counter.methods.increase.next(state);
const deployed = await counter.deploy(state, { provider, satoshis: 1_000n });

let spend: Promise<unknown> | undefined;
expect(() => {
spend = deployed.methods.increase.call({
provider,
nextState,
});
}).not.to.throw();
await spend;
});
});

Run a focused docs-style test file with:

npx mocha --no-config --require tsx test/p2pkh.test.ts

MemoryProvider Integration

Use createMemoryProvider() after local BVM verification passes. It lets the same bound .deploy() and deployed method .call() code run without a chain backend, and realistic mode adds wallet funding and change handling. Keep these tests in Mocha too: they are still offline tests, but they exercise a larger surface than verify(...). In contract-facing docs, assert that the method call does not throw. Reserve txid shape, UTXO inventory, and fee/change assertions for dedicated runtime/provider tests.

Test Shape

  • Keep compile-only tests near the contract definition.
  • Prefer .verify(...) for method-level script checks before provider-backed tests.
  • Use memory-provider E2E tests for deploy and spend behavior.
  • Use randomized fixture values for state transitions that depend on arithmetic boundaries.
  • Assert both success and expected rejection messages.
note

The TicTacToe runtime tests are the best current model for stateful board fixtures and branch coverage.

What's Next