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:
- Unit tests — your Tolk consumer logic in isolation
- Integration tests — your consumer + a mocked oracle, end-to-end
- 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.
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:
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';
Integration testing — full lifecycle
For confidence that your consumer interacts correctly with the real OMY contracts, run the full assertion lifecycle in the sandbox:
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:
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:
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:
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';