From a0feb60f961657b1387b2bc8d4f28ed409f00468 Mon Sep 17 00:00:00 2001 From: skywardboundd Date: Fri, 14 Nov 2025 17:02:55 +0300 Subject: [PATCH 1/7] feat: add testing styleguide --- contract-dev/testing/styleguide.mdx | 292 +++++++++++++++++++++++++++- 1 file changed, 289 insertions(+), 3 deletions(-) diff --git a/contract-dev/testing/styleguide.mdx b/contract-dev/testing/styleguide.mdx index d5505a65e..533b2cf67 100644 --- a/contract-dev/testing/styleguide.mdx +++ b/contract-dev/testing/styleguide.mdx @@ -1,7 +1,293 @@ --- -title: "Styleguide" +title: "Testing styleguide" +sidebarTitle: "Styleguide" --- -import { Stub } from '/snippets/stub.jsx'; +import { Aside } from '/snippets/aside.jsx'; - +Your smart contract works perfectly in testnet, but in mainnet it suddenly loses user funds. Or worse: tests pass today, fail tomorrow, and the failure reason remains elusive. These scenarios have challenged every TON developer at some point. + +This guide explores proven patterns for writing tests that serve as reliable safeguards rather than sources of frustration. Through real-world examples and practical recommendations, it examines how to build test suites that catch issues before they reach production. + + + +## Essential patterns + +### Use descriptive test names with "should" + +When a test fails, the description should instantly tell what broke. Vague names force developers to read the entire test body to understand what was being tested. + +Test descriptions must clearly state what behavior is being verified. Use the pattern "should [action or outcome]" to make test intent immediately obvious. + +```typescript +// ❌ Bad - vague or incomplete descriptions +it('counter', async () => { }); + +it('test increment', async () => { }); + +// ✅ Good - clear "should" pattern +it('should increment counter', async () => { }); + +it('should reject increment from non-owner', async () => { }); +``` + +### Prefer `const` over `let` + +A common scenario: A developer writes tests using `let`, later adds another `beforeEach` hook, and accidentally redefines a variable. Half the test suite starts using the wrong contract instance. Three hours of debugging follow. + +Use `const` for all variable declarations. Immutable bindings eliminate accidental reassignments and make code behavior predictable. + +```typescript expandable +// ❌ Bad - using let +let blockchain: Blockchain; +let contract: SandboxContract; + +beforeEach(async () => { + blockchain = await Blockchain.create(); + contract = blockchain.openContract(await Contract.fromInit()); +}); + +it('should do smth', async () => { + await contract.sendSmth(); +}); +``` + +```typescript expandable +// ✅ Good - using const +it('should do smth', async () => { + const blockchain = await Blockchain.create({ config: "slim" }); + const contract = blockchain.openContract(await Contract.fromInit()); + + await contract.sendSmth(); +}); +``` + +### Do not depend on state generated by previous tests + +Test execution order can change. Dependencies between tests create fragile suites where one failure cascades into dozens of false failures, making debugging a nightmare. + +Each test should be completely independent and not rely on state from other tests. + +```typescript expandable +// ❌ Bad - dependent tests +describe('Contract operations', () => { + let blockchain: Blockchain; + let contract: SandboxContract; + + beforeAll(async () => { + blockchain = await Blockchain.create(); + contract = blockchain.openContract(MyContract.createFromConfig({}, code)); + await contract.sendDeploy(deployer.getSender(), toNano('0.05')); + }); + + it('should increment counter', async () => { + await contract.sendIncrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(1n); + }); + + it('should decrement counter', async () => { + // This test depends on the previous test's state + await contract.sendDecrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(0n); // Assumes counter was 1 + }); +}); +``` + +``` typescript expandable +// ✅ Good - independent tests +describe('Contract operations', () => { + it('should increment counter from zero', async () => { + const blockchain = await Blockchain.create(); + const contract = blockchain.openContract(MyContract.createFromConfig({}, code)); + await contract.sendDeploy(deployer.getSender(), toNano('0.05')); + + await contract.sendIncrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(1n); + }); + + it('should decrement counter from one', async () => { + const blockchain = await Blockchain.create(); + const contract = blockchain.openContract(MyContract.createFromConfig({}, code)); + await contract.sendDeploy(deployer.getSender(), toNano('0.05')); + + // Set up the required state + await contract.sendIncrement(user.getSender(), toNano('0.1')); + + await contract.sendDecrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(0n); + }); +}); +``` + +### Single expect per test + +When a test has three assertions and fails on the second, the third never runs. This masks additional bugs and forces sequential debugging instead of catching all issues at once. + +Each test should verify one specific behavior. Multiple assertions make it harder to identify what exactly failed. + +```typescript expandable +// ❌ Bad - multiple expectations +it('should handle user operations', async () => { + // some code + await contract.sendIncrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(1n); + + await contract.sendDecrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(0n); + + expect(await contract.getOwner()).toEqualAddress(user.address); +}); +``` + +```typescript expandable +// ✅ Good - single expectation per test +it('should increment counter', async () => { + // some code + await contract.sendIncrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(1n); +}); + +it('should decrement counter', async () => { + // some code + await contract.sendIncrement(user.getSender(), toNano('0.1')); + await contract.sendDecrement(user.getSender(), toNano('0.1')); + expect(await contract.getCounter()).toBe(0n); +}); + +it('should set correct owner', async () => { + // some code + expect(await contract.getOwner()).toEqualAddress(user.address); +}); +``` + +### Extract shared logic into test functions + +When multiple contracts share similar behavior, extract that logic into a reusable test function. This reduces duplication and ensures consistent testing across related contracts. + +See example in the [HotUpdate test suite](https://github.com/ton-org/docs-examples/blob/main/contract-dev/Upgrading/tests/HotUpdate.spec.ts). + +### Use `setup()` function + +Tests with duplicated setup code become maintenance burdens. When initialization logic changes, updating it in 20 places invites errors and inconsistency. + +Extract common test setup into a dedicated function to reduce duplication and improve maintainability. + +```typescript expandable +const setup = async () => { + const blockchain = await Blockchain.create({ config: "slim" }); + + const owner = await blockchain.treasury("deployer"); + const user = await blockchain.treasury("user"); + + const contract = blockchain.openContract(await Parent.fromInit()); + + const deployResult = await contract.send(owner.getSender(), { value: toNano(0.5) }, null); + + return { blockchain, owner, user, contract, deployResult }; +}; + +it("should deploy correctly", async () => { + const { owner, contract, deployResult } = await setup(); + + expect(deployResult.transactions).toHaveTransaction({ + from: owner.address, + to: contract.address, + deploy: true, + success: true, + }); +}); +``` + +### Component-based test organization + +Large protocols have individual contract tests and integration tests. Separating concerns makes it clear what broke. + +Organize tests by functional components for better structure. + +```typescript +describe("parent", () => { + // Test parent contract in isolation +}); + +describe("child", () => { + // Test child contract in isolation +}); + +describe("protocol", () => { + // Test end-to-end protocol flows +}); +``` + +## Gas and fee considerations + +Fee validation is critical in TON because transactions can halt mid-execution when funds run out. Consider this scenario with [Jetton](/standard/tokens/jettons/how-it-works) transfers: + +Alice sends 100 jettons to Bob. The transaction successfully debits Alice's jetton wallet, but Bob never receives the tokens: insufficient fees prevented the message from reaching Bob's wallet. The tokens are effectively lost. + +This demonstrates why thorough fee testing is essential. Always validate that your gas constants and fee calculations are sufficient for complete transaction execution. See more details in our [gas documentation](/contract-dev/gas). + +### Discovering minimal fees + +While you can calculate required fees using formulas, empirically discovering the minimal amount provides practical validation. The following pattern uses binary search to efficiently find the threshold: + +```typescript expandable +test.skip("find minimal amount of TON for protocol", async () => { + const checkAmount = async (amount: bigint) => { + const { user, child, parent } = await setup(); + const message: SomeMessage = { $$type: "SomeMessage" }; + const sendResult = await child.send(user.getSender(), { value: amount }, message); + + expect(sendResult.transactions).toHaveTransaction({ + from: parent.address, + to: user.address, + body: beginCell().endCell(), + mode: SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE + SendMode.IGNORE_ERRORS, + }); + }; + + let L = 0n; + let R = toNano(10); + + while (L + 1n < R) { + let M = (L + R) / 2n; + + try { + await checkAmount(M); + R = M; + } catch (error) { + L = M; + } + } + + console.log(R, "is the minimal amount of nanotons for protocol"); +}); +``` + +## Advanced testing patterns + +### Testing all message fields + +Messages are the backbone of TON contracts. A wrong address, incorrect mode, or malformed body can cause funds to disappear or protocol invariants to break. Comprehensive field checks catch these issues. + +When testing messages, verify all important fields to ensure complete correctness. + +```typescript expandable +it("should send message to parent", async () => { + const { user, owner, contract } = await setup(); + const message: SomeMessage = { $$type: "SomeMessage" }; + const sendResult = await contract.send(user.getSender(), { value: toNano(0.1) }, message); + const expectedMessage: NotifyParent = { + $$type: "NotifyParent", + originalSender: user.address, + }; + const expectedBody = beginCell().store(storeNotifyParent(expectedMessage)).endCell(); + expect(sendResult.transactions).toHaveTransaction({ + from: contract.address, + to: owner.address, + body: expectedBody, + mode: SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE, + }); +}); +``` From af9a55617c26835a0e095fab8904ddd12e81c1b1 Mon Sep 17 00:00:00 2001 From: skywardboundd Date: Fri, 14 Nov 2025 17:04:56 +0300 Subject: [PATCH 2/7] fmt --- contract-dev/testing/styleguide.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contract-dev/testing/styleguide.mdx b/contract-dev/testing/styleguide.mdx index 533b2cf67..3e29b4c32 100644 --- a/contract-dev/testing/styleguide.mdx +++ b/contract-dev/testing/styleguide.mdx @@ -10,7 +10,7 @@ Your smart contract works perfectly in testnet, but in mainnet it suddenly loses This guide explores proven patterns for writing tests that serve as reliable safeguards rather than sources of frustration. Through real-world examples and practical recommendations, it examines how to build test suites that catch issues before they reach production. ## Essential patterns @@ -19,9 +19,9 @@ Full code examples from this guide are available in the [testing styleguide repo When a test fails, the description should instantly tell what broke. Vague names force developers to read the entire test body to understand what was being tested. -Test descriptions must clearly state what behavior is being verified. Use the pattern "should [action or outcome]" to make test intent immediately obvious. +Test descriptions must clearly state what behavior is being verified. Use the pattern "should \[action or outcome]" to make test intent immediately obvious. -```typescript +```typescript // ❌ Bad - vague or incomplete descriptions it('counter', async () => { }); @@ -95,7 +95,7 @@ describe('Contract operations', () => { }); ``` -``` typescript expandable +```typescript expandable // ✅ Good - independent tests describe('Contract operations', () => { it('should increment counter from zero', async () => { From cea1677a2bd3d7911cf3e3b635a0711c0abd9541 Mon Sep 17 00:00:00 2001 From: Andrew <58519828+skywardboundd@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:27:04 +0300 Subject: [PATCH 3/7] Update contract-dev/testing/styleguide.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- contract-dev/testing/styleguide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-dev/testing/styleguide.mdx b/contract-dev/testing/styleguide.mdx index 3e29b4c32..0504d81fd 100644 --- a/contract-dev/testing/styleguide.mdx +++ b/contract-dev/testing/styleguide.mdx @@ -5,7 +5,7 @@ sidebarTitle: "Styleguide" import { Aside } from '/snippets/aside.jsx'; -Your smart contract works perfectly in testnet, but in mainnet it suddenly loses user funds. Or worse: tests pass today, fail tomorrow, and the failure reason remains elusive. These scenarios have challenged every TON developer at some point. +A smart contract works perfectly on testnet, but on mainnet it suddenly loses user funds. Or worse: tests pass today, fail tomorrow, and the failure reason remains elusive. These scenarios have challenged TON developers at some point. This guide explores proven patterns for writing tests that serve as reliable safeguards rather than sources of frustration. Through real-world examples and practical recommendations, it examines how to build test suites that catch issues before they reach production. From ad817bd6300f81acbe97173e7d6ad1b455c0318f Mon Sep 17 00:00:00 2001 From: Andrew <58519828+skywardboundd@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:27:32 +0300 Subject: [PATCH 4/7] Update contract-dev/testing/styleguide.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- contract-dev/testing/styleguide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-dev/testing/styleguide.mdx b/contract-dev/testing/styleguide.mdx index 0504d81fd..94ff35533 100644 --- a/contract-dev/testing/styleguide.mdx +++ b/contract-dev/testing/styleguide.mdx @@ -7,7 +7,7 @@ import { Aside } from '/snippets/aside.jsx'; A smart contract works perfectly on testnet, but on mainnet it suddenly loses user funds. Or worse: tests pass today, fail tomorrow, and the failure reason remains elusive. These scenarios have challenged TON developers at some point. -This guide explores proven patterns for writing tests that serve as reliable safeguards rather than sources of frustration. Through real-world examples and practical recommendations, it examines how to build test suites that catch issues before they reach production. +Apply testing patterns that build reliable suites and catch issues before production.