Skip to main content

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

Testing

Production integrations always test the unhappy paths. This page covers the three layers of testing every OMY consumer should have:

  1. Unit tests — your Tolk consumer logic in isolation
  2. Integration tests — your consumer + a mocked oracle, end-to-end
  3. End-to-end smoke tests — your consumer against the live factory

The setup

OMY contracts use Acton — TON's official testing + deployment toolchain. If you're writing a Tolk consumer, adopting Acton's sandbox gives you the cleanest test ergonomics.

install Acton
curl -fsSL https://acton.dev/install.sh | sh
acton --version

For TypeScript-only consumers (off-chain bots, indexers), you don't need Acton — @ton/sandbox works fine.

Unit testing your Tolk consumer

Acton's testing framework uses *.test.tolk files. The pattern is to mock the oracle's OracleResult callback directly to your consumer:

tests/my-consumer.test.tolk
import "@acton/emulation/network"
import "@acton/emulation/testing"
import "@acton/testing/expect"

import "@contracts/oracle-messages" // OracleResult
import "@wrappers/MyConsumer.gen"

get fun `test: consumer accepts OracleResult from bound assertion`() {
val owner = testing.treasury("owner");
val oracle = testing.treasury("mock-oracle");

val consumer = MyConsumer.fromStorage({
owner: owner.address,
boundOracle: oracle.address,
settledQuestions: [],
// ... your storage ...
});
consumer.deploy(owner.address, { value: ton("0.5") });

// Send OracleResult AS the bound oracle — anti-spoof check passes
val res = consumer.sendOracleResult(
oracle.address,
questionId: 1,
answer: true,
{ value: ton("0.1") }
);
expect(res).toHaveSuccessfulTx({ to: consumer.address });

// Verify dedupe — second delivery is a no-op (status check)
val replay = consumer.sendOracleResult(oracle.address, 1, true, { value: ton("0.1") });
expect(replay).toHaveSuccessfulTx({ to: consumer.address }); // accepted but no-op
// assert balance/state didn't change
}

get fun `test: consumer rejects OracleResult from non-bound sender`() {
val owner = testing.treasury("owner");
val realOracle = testing.treasury("real-oracle");
val attacker = testing.treasury("attacker");

val consumer = MyConsumer.fromStorage({
owner: owner.address,
boundOracle: realOracle.address,
settledQuestions: [],
});
consumer.deploy(owner.address, { value: ton("0.5") });

// Attacker tries to spoof — anti-spoof check fires
val res = consumer.sendOracleResult(
attacker.address,
questionId: 1,
answer: true,
{ value: ton("0.1") }
);
expect(res).toHaveFailedTx({ to: consumer.address, exitCode: MyErr.NotOurOracle });
}

The key tests every consumer should have:

import {DocFeatures, DocFeature} from '@site/src/components/docs/DocFeatures';

Send OracleResult from a non-bound sender — must throw NotOurOracle. Send the same OracleResult twice — second call is a no-op, state unchanged. Simulate TimeoutRefund landing — verify your consumer handles "no answer" gracefully (refund users, etc.). If you have CancelAwaiting, test that it rejects before deadline + after a bound assertion arrives. Track jetton balances before/after. Sum must conserve to the nano. Send underfunded transfers, wrong wallets, malformed payloads — verify the contract refunds (doesn't throw).

Integration testing — full lifecycle

For confidence that your consumer interacts correctly with the real OMY contracts, run the full assertion lifecycle in the sandbox:

tests/full-lifecycle.test.tolk
import "@contracts/Assertion"
import "@contracts/OracleFactory"
import "@contracts/CommitteeResolver"

get fun `test: e2e — request through finalize`() {
val owner = testing.treasury("owner");
val proposer = testing.treasury("proposer");

// 1. Deploy the full OMY stack: minter, committee, factory
val (minter, ...) = setupTest();
val committee = deployCommittee(owner, threshold: 1);
val factory = deployFactory(owner, minter, committee);

// 2. Deploy YOUR consumer pointing at THIS factory
val consumer = MyConsumer.fromStorage({
oracleFactory: factory.address,
// ...
});
consumer.deploy(owner.address, {value: ton("1")});

// 3. Trigger your consumer's request flow
consumer.sendRequest(owner.address, ...);

// 4. Mint USDT to proposer + propose
val proposerWallet = mintTo(minter, owner, proposer.address, ton("100"));
sendBond(proposerWallet, assertionAddr, proposer.address, ton("3"), OP_PROPOSE_TRUE);

// 5. Skip past liveness, finalize
testing.setNow(START + LIVENESS + 1);
assertion.sendFinalize(owner.address, {value: ton("0.1")});

// 6. Assert your consumer received OracleResult and reacted correctly
expect(consumer.status()).toEqual(SETTLED);
}

This is a long test (lots of setup), but it catches the cross-contract integration bugs that unit tests miss — like wrong factory address, wrong opcodes, mistyped TL-B fields.

Off-chain testing

For TypeScript consumers (indexers, settler bots), use @ton/sandbox to emulate the chain locally:

tests/oracle.test.ts
import {Blockchain} from '@ton/sandbox';
import {Address, beginCell, toNano} from '@ton/core';
import {parseOracleResult} from '../src/omy';

describe('oracle result handling', () => {
let blockchain: Blockchain;

beforeEach(async () => {
blockchain = await Blockchain.create();
});

it('dedupes by questionId in database', async () => {
const handler = new OracleResultHandler(db);

await handler.process({questionId: 1n, answer: true});
await handler.process({questionId: 1n, answer: true}); // duplicate
await handler.process({questionId: 1n, answer: false}); // contradiction

// Only the first delivery effected business logic
expect(await db.getSettlement(1n)).toEqual({answer: true});
expect(await db.getAuditLog(1n)).toHaveLength(1);
});

it('parses inbound bodies correctly', () => {
const body = beginCell()
.storeUint(0x0a02, 32)
.storeUint(42n, 64)
.storeBit(true)
.endCell();

const parsed = parseOracleResult(body);
expect(parsed).toEqual({questionId: 42n, answer: true});
});

it('rejects non-matching opcodes', () => {
const body = beginCell().storeUint(0x1234, 32).endCell();
expect(parseOracleResult(body)).toBeNull();
});
});

Mocking the oracle in your tests

Don't deploy the real factory in every test — it's slow. Mock the OracleResult callback directly:

// In your test setup, instead of going through the full assertion flow:
val mockOracle = testing.treasury("mock-oracle");
consumer.bindOracle(mockOracle.address); // your bind path

// Now you can fire arbitrary OracleResults straight to your consumer:
consumer.sendOracleResult(mockOracle.address, questionId: 1, answer: true, ...);
consumer.sendOracleResult(mockOracle.address, questionId: 2, answer: false, ...);

Use the full lifecycle test for cross-contract correctness; use mocked oracle tests for your business logic under different oracle outcomes.

End-to-end smoke

After unit + integration green, run against the live factory before going into production. This catches:

  • Wrong contract addresses in your env
  • Gas miscalculations under real network fees
  • TonConnect / wallet integration issues
  • API rate limits

The OMY repo includes scenario-*.tolk scripts that walk through the happy path + dispute path. Mirror them for your consumer:

e2e smoke
DEPLOYER=deployer FACTORY=$TONOMY_FACTORY \
acton script scripts/your-consumer-smoke.tolk

Track each tx hash; verify on a TON explorer that your consumer's state transitions are correct.

CI integration

Run Acton + Jest tests on every PR:

.github/workflows/test.yml
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: {node-version: 20}
- run: curl -fsSL https://acton.dev/install.sh | sh
- run: acton test
- run: npm ci
- run: npm test

Next

import {DocCards, DocCard} from '@site/src/components/docs/DocCards';