-
Notifications
You must be signed in to change notification settings - Fork 24
feat: add testing styleguide #1241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
skywardboundd
wants to merge
12
commits into
main
Choose a base branch
from
166-ecosystem-testing-styleguide
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a0feb60
feat: add testing styleguide
skywardboundd af9a556
fmt
skywardboundd 102d404
Merge branch 'main' into 166-ecosystem-testing-styleguide
skywardboundd cea1677
Update contract-dev/testing/styleguide.mdx
skywardboundd ad817bd
Update contract-dev/testing/styleguide.mdx
skywardboundd ad5ffe3
Merge branch 'main' into 166-ecosystem-testing-styleguide
anton-trunov 1ad94c9
Merge branch 'main' into 166-ecosystem-testing-styleguide
anton-trunov 51c8f00
review
skywardboundd 2b279bd
Merge branch 'main' into 166-ecosystem-testing-styleguide
skywardboundd 6bd0579
fix
skywardboundd 4ab8e5e
Merge branch 'main' into 166-ecosystem-testing-styleguide
skywardboundd 7ce8000
Update contract-dev/testing/style-guide.mdx
skywardboundd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| --- | ||
| title: "Testing styleguide" | ||
| sidebarTitle: "Styleguide" | ||
| --- | ||
|
|
||
| import { Aside } from '/snippets/aside.jsx'; | ||
|
|
||
| This guide explains how to write tests for TON smart contracts. | ||
|
|
||
| <Aside type="tip"> | ||
| Full code examples from this guide are available in the [testing styleguide repository](https://github.com/ton-org/docs-examples/tree/main/Testing/Styleguide). | ||
| </Aside> | ||
|
|
||
| ### Use descriptive test names with "should" | ||
|
|
||
| Name tests using the "should" pattern. This makes test intent clear and failures easy to understand. | ||
|
|
||
| ```typescript | ||
skywardboundd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // ❌ 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` | ||
|
|
||
| Use `const` for all variable declarations. Immutable bindings prevent accidental reassignments and make test behavior predictable. Tests using `let` with shared hooks can silently use wrong contract instances when setup changes. | ||
|
|
||
| ```typescript expandable | ||
| // ❌ Bad - using let | ||
| let blockchain: Blockchain; | ||
| let contract: SandboxContract<Contract>; | ||
|
|
||
| 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 | ||
|
|
||
| Make each test completely independent. Test execution order can change. Dependencies between tests create fragile suites where one failure cascades into dozens of false failures. | ||
|
|
||
| ```typescript expandable | ||
| // ❌ Bad - dependent tests | ||
| describe('Contract operations', () => { | ||
| let blockchain: Blockchain; | ||
| let contract: SandboxContract<MyContract>; | ||
|
|
||
| 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 | ||
|
|
||
| Verify one specific behavior per test. When a test has multiple 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. | ||
|
|
||
| ```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 | ||
|
|
||
| Extract common test setup into a dedicated function. This reduces duplication and improves maintainability. When initialization logic changes, updating it in one place prevents errors and inconsistency. | ||
|
|
||
| ```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 | ||
|
|
||
| Organize tests by functional components. Large protocols have individual contract tests and integration tests. Separating concerns makes it clear what broke. | ||
|
|
||
| ```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. | ||
|
|
||
| Always validate that gas constants and fee calculations are sufficient for complete transaction execution. See more details in the [gas documentation](/contract-dev/gas). | ||
|
|
||
| ### Discovering minimal fees | ||
|
|
||
| Calculate required fees using formulas, but empirically discovering the minimal amount provides practical validation. Use 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 | ||
|
|
||
| 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, | ||
| }); | ||
| }); | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.