Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
587b516
feat: add test stub generation
skywardboundd Jun 2, 2025
aafb8dc
fmt
skywardboundd Jun 2, 2025
621821a
may be fix
skywardboundd Jun 2, 2025
a9dc455
fmt
skywardboundd Jun 2, 2025
95fde95
x2 fix
skywardboundd Jun 2, 2025
2f0e22c
fix getter names
skywardboundd Jun 2, 2025
cdb4077
fix :(
skywardboundd Jun 2, 2025
55f5585
use cleanText in text recievers
skywardboundd Jun 2, 2025
9cd771c
back eslintrc
skywardboundd Jun 2, 2025
7aea274
use mustache
skywardboundd Jun 4, 2025
c02a836
fmt
skywardboundd Jun 4, 2025
d2e85d9
review
skywardboundd Jun 5, 2025
15b9eef
Merge remote-tracking branch 'origin/main' into 3287-we-should-genera…
skywardboundd Jun 5, 2025
918d13c
fix
skywardboundd Jun 5, 2025
ab166e8
upd
skywardboundd Jun 5, 2025
df7ea9e
fix
skywardboundd Jun 5, 2025
743c6bd
fix
skywardboundd Jun 6, 2025
eec818c
fmt
skywardboundd Jun 6, 2025
7d7c54c
Merge branch 'main' into 3287-we-should-generate-test-template-as-wra…
skywardboundd Jun 6, 2025
7e67feb
fix windows shvindows bobush vobush
skywardboundd Jun 6, 2025
3da6ee7
Merge branch '3287-we-should-generate-test-template-as-wrappers' of h…
skywardboundd Jun 6, 2025
8c5945e
fmt aggaaain
skywardboundd Jun 6, 2025
916dec5
update docs
skywardboundd Jun 6, 2025
e885c60
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
ada9062
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
39d2d0f
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
ce0bb6a
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
4c33fb7
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
40cd7a7
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
c94d3f6
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
e946949
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
5ecc307
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
e12ec2c
Update docs/src/content/docs/book/config.mdx
skywardboundd Jun 6, 2025
e6ad976
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/src/content/docs/book/compile.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,45 @@ export class Playground implements Contract {

:::

### Test generation, `.stub.tests.ts` {#test-stubs}

<Badge text="Available since Tact 1.6.14" variant="tip" size="medium"/><p/>

By default, Tact automatically generates test stubs for each compiled contract. These files provide a basic testing structure using TypeScript and the TON Blockchain sandbox, serving as a starting point for comprehensive contract testing.

#### Generated file structure {#test-file-structure}

Test stubs are saved to the `tests/` subdirectory inside the output folder specified in the [configuration](/book/config#projects-output). Each contract gets its test file named as `{project_name}_{contract_name}.stub.tests.ts`.

For example: `build/tests/MyProject_Counter.stub.tests.ts`, where `build/` is the output folder.

#### Test content {#test-stub-content}

Generated test stubs include basic blockchain sandbox setup, contract deployment tests, and examples using the [TypeScript wrappers](#wrap-ts). They provide a foundation for testing contract functionality with proper imports and configuration.

#### Configuration {#test-generation-config}

Disable automatic test generation by adding the `skipTestGeneration` option to your [project configuration](/book/config):

```json title="tact.config.json"
{
"projects": [
{
"name": "MyProject",
"path": "./contracts/contract.tact",
"output": "./build",
"options": {
"skipTestGeneration": true
}
}
]
}
```

#### Using test stubs {#test-stub-usage}

Read more in the dedicated section: [Using generated test stubs](/book/debug#tests-using-stubs).

[struct]: /book/structs-and-messages#structs
[message]: /book/structs-and-messages#messages

Expand Down
38 changes: 37 additions & 1 deletion docs/src/content/docs/book/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,41 @@ If set to `true{:json}`, enables the generation of the `lazy_deployment_complete
}
```

#### `skipTestGeneration` {#options-skiptestgeneration}

<Badge text="Available since Tact 1.6.14" variant="tip" size="medium"/><p/>

`false{:json}` by default.

If set to `true{:json}`, disables automatic generation of test files. By default, Tact generates test stubs for all contracts in the `tests/` subdirectory of the output folder.

```json title="tact.config.json" {8,14}
{
"projects": [
{
"name": "contract",
"path": "./contract.tact",
"output": "./output",
"options": {
"skipTestGeneration": true
}
},
{
"name": "ContractUnderBlueprint",
"options": {
"skipTestGeneration": true
}
}
]
}
```

:::note

Read more about test generation: [Test generation](/book/compile#test-stubs).

:::

### `verbose` {#projects-verbose}

<Badge text="Available since Tact 1.6" variant="tip" size="medium"/><p/>
Expand Down Expand Up @@ -595,7 +630,8 @@ In [Blueprint][bp], `mode` is always set to `"full"{:json}` and cannot be overri
"alwaysSaveContractData": true,
"internalExternalReceiversOutsideMethodsMap": true
},
"enableLazyDeploymentCompletedGetter": true
"enableLazyDeploymentCompletedGetter": true,
"skipTestGeneration": false
}
}
]
Expand Down
24 changes: 24 additions & 0 deletions docs/src/content/docs/book/debug.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ Whenever you create a new [Blueprint][bp] project or use the `blueprint create`

Those files are placed in the `tests/` folder and executed with [Jest][jest]. By default, all tests run unless you specify a specific group or test closure. For other options, refer to the brief documentation in the Jest CLI: `jest --help`.

### Using generated test stubs {#tests-using-stubs}

<Badge text="Available since Tact 1.6.14" variant="tip" size="medium"/><p/>

Tact automatically generates test stubs for each compiled contract during the [compilation process](/book/compile#test-stubs). These generated test files serve as excellent starting points for writing comprehensive tests.

To use the generated test stubs:

1. Copy them from the `tests/` subdirectory inside the output directory specified in your [`tact.config.json`](/book/config#projects-output).

2. Customize them according to the specific needs of your contract. The generated stubs include:

* Basic [Sandbox][sb] setup
* Contract deployment tests
* Example use of [TypeScript wrappers](#tests-wrappers)

3. Extend with additional test cases. The generated tests are intended as a starting point, not a complete test suite.

:::caution

Since generated test files are overwritten on each compilation, always copy them to a separate location **before customizing**. This ensures your test modifications are preserved.

:::

### Structure of test files {#tests-structure}

Let's say we have a contract named `Playground`, written in the `contracts/playground.tact` file. If we've created that contract through [Blueprint][bp], it also created a `tests/Playground.spec.ts` test suite file for us.
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
],
"scripts": {
"compare": "ts-node src/logs/compare-logs.infra.ts",
"build:fast": "yarn clean && yarn gen:grammar && yarn gen:stdlib && yarn gen:func-js && tsc --project tsconfig.fast.json && yarn copy:stdlib && yarn copy:func && yarn to-relative",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=5120 tsc && yarn copy:stdlib && yarn copy:func && yarn to-relative",
"build:fast": "yarn clean && yarn gen:grammar && yarn gen:stdlib && yarn gen:func-js && tsc --project tsconfig.fast.json && yarn copy:stdlib && yarn copy:func && yarn copy:templates && yarn to-relative",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=5120 tsc && yarn copy:stdlib && yarn copy:func && yarn copy:templates && yarn to-relative",
"gen:config": "ts-to-zod -k --skipValidation src/config/config.ts src/config/config.zod.ts",
"gen:grammar": "pgen src/grammar/grammar.peggy -o src/grammar/grammar.ts",
"gen:stdlib": "ts-node src/stdlib/stdlib.build.ts",
Expand All @@ -39,6 +39,7 @@
"cleanall": "rimraf dist node_modules",
"copy:stdlib": "ts-node src/stdlib/copy.build.ts",
"copy:func": "ts-node src/func/copy.build.ts",
"copy:templates": "ts-node src/bindings/copy.build.ts",
"to-absolute": "ts-node src/to-absolute.build.ts",
"to-relative": "ts-node src/to-relative.build.ts",
"test": "jest",
Expand Down Expand Up @@ -98,6 +99,7 @@
"blockstore-core": "1.0.5",
"glob": "^8.1.0",
"ipfs-unixfs-importer": "9.0.10",
"mustache": "^4.2.0",
"path-normalize": "^6.0.13",
"yaml": "^2.7.1",
"zod": "^3.22.4"
Expand All @@ -118,6 +120,7 @@
"@types/diff": "^7.0.0",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.12",
"@types/mustache": "^4.2.6",
"@types/node": "^22.5.0",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
Expand Down
30 changes: 30 additions & 0 deletions src/bindings/copy.build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as glob from "glob";

const cp = async (fromGlob: string, toPath: string) => {
for (const file of glob.sync(path.join(fromGlob, "**/*"), {
windowsPathsNoEscape: true,
})) {
const relPath = path.relative(fromGlob, file);
const pathTo = path.join(toPath, relPath);
const stat = await fs.stat(file);
if (stat.isDirectory()) {
await fs.mkdir(pathTo, { recursive: true });
} else {
await fs.mkdir(path.dirname(pathTo), { recursive: true });
await fs.copyFile(file, pathTo);
}
}
};

const main = async () => {
try {
await cp("./src/bindings/templates/", "./dist/bindings/templates/");
} catch (e) {
console.error(e);
process.exit(1);
}
};

void main();
48 changes: 48 additions & 0 deletions src/bindings/templates/test.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// !!THIS FILE IS GENERATED BY TACT. THIS FILE IS REGENERATED EVERY TIME, COPY IT TO YOUR PROJECT MANUALLY!!
// https://docs.tact-lang.org/book/debug/

{{#imports}}
import {{{.}}};
{{/imports}}

export const test{{contractName}} = () => {
describe("{{contractName}} Contract", () => {
// Test receivers
{{#receivers}}
test{{.}}();
{{/receivers}}
// Test getters
{{#getters}}
getterTest{{.}}();
{{/getters}}
});
};

const globalSetup = async () => {
const blockchain = await Blockchain.create();
// @ts-ignore
const contract = await blockchain.openContract(await {{contractName}}.fromInit(
// TODO: implement default values
));

// Universal method for deploy contract without sending message
await blockchain.setShardAccount(contract.address, createShardAccount({
address: contract.address,
code: contract.init!.code,
data: contract.init!.data,
balance: 0n,
workchain: 0
}));

const owner = await blockchain.treasury("owner");
const notOwner = await blockchain.treasury("notOwner");

return { blockchain, contract, owner, notOwner };
};

{{#receiverBlocks}}
{{{.}}}
{{/receiverBlocks}}
{{#getterBlocks}}
{{{.}}}
{{/getterBlocks}}
114 changes: 114 additions & 0 deletions src/bindings/writeTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { readFileSync } from "fs";
import { resolve } from "path";
import Mustache from "mustache";
import type { ABIArgument, ContractABI, ABIReceiver } from "@ton/core";
import type { WrappersConstantDescription } from "@/bindings/writeTypescript";
import type { CompilerContext } from "@/context/context";
import type { TypeDescription } from "@/types/types";

function getReceiverFunctionName(receiver: ABIReceiver): string {
const receiverType = receiver.receiver; // 'internal' or 'external'
const messageKind = receiver.message.kind; // 'empty', 'typed', 'text', 'any'

let name = receiverType.charAt(0).toUpperCase() + receiverType.slice(1); // Internal or External

switch (messageKind) {
case "empty":
name += "Empty";
break;
case "typed":
name += "Message";
name += receiver.message.type ?? "Typed";
break;
case "text":
name += "Text";
if (receiver.message.text) {
const cleanText = receiver.message.text.replace(
/[^a-zA-Z0-9]/g,
"",
);
name += cleanText.charAt(0).toUpperCase() + cleanText.slice(1);
}
break;
case "any":
name += "Any";
break;
default:
name += "Unknown";
}

return name;
}

export function writeTests(
abi: ContractABI,
_ctx: CompilerContext,
_constants: readonly WrappersConstantDescription[],
_contract: undefined | TypeDescription,
generatedContractPath: string,
_init?: {
code: string;
system: string | null;
args: ABIArgument[];
prefix?:
| {
value: number;
bits: number;
}
| undefined;
},
) {
const contractName = abi.name ?? "Contract";

const templateData = {
contractName,
imports: [
`{ ${contractName} } from '../${generatedContractPath}'`,
'{ Blockchain, createShardAccount } from "@ton/sandbox"',
],
receivers: abi.receivers?.map(getReceiverFunctionName) ?? [],
getters: abi.getters?.map((g) => g.name) ?? [],
receiverBlocks: (abi.receivers ?? []).map((r) => {
const fn = getReceiverFunctionName(r);
return `const test${fn} = async () => {
describe("${fn}", () => {
const setup = async () => {
return await globalSetup();
};

// !!THIS FILE IS GENERATED BY TACT. THIS FILE IS REGENERATED EVERY TIME, COPY IT TO YOUR PROJECT MANUALLY!!
// TODO: You can write tests for ${fn} here

it("should perform correctly", async () => {
const { blockchain, contract, owner, notOwner } = await setup();
});
});
};
`;
}),
getterBlocks: (abi.getters ?? []).map((g) => {
const fn = g.name;
return `const getterTest${fn} = async () => {
describe("${fn}", () => {
const setup = async () => {
return await globalSetup();
};

// !!THIS FILE IS GENERATED BY TACT. THIS FILE IS REGENERATED EVERY TIME, COPY IT TO YOUR PROJECT MANUALLY!!
// TODO: You can write tests for ${fn} here

it("should perform correctly", async () => {
const { blockchain, contract, owner, notOwner } = await setup();
});
});
};
`;
}),
};

const templatePath = resolve(__dirname, "templates", "test.mustache");
const template = readFileSync(templatePath, "utf-8");
const rendered = Mustache.render(template, templateData);

return rendered;
}
4 changes: 4 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export type Options = {
* Does nothing if contract parameters are declared.
*/
readonly enableLazyDeploymentCompletedGetter?: boolean;
/**
* If set to true, disables generation of test files.
*/
readonly skipTestGeneration?: boolean;
};

export type Mode = "fullWithDecompilation" | "full" | "funcOnly" | "checkOnly";
Expand Down
4 changes: 4 additions & 0 deletions src/config/config.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export const optionsSchema: z.ZodType<C.Options> = z.object({
* Does nothing if contract parameters are declared.
*/
enableLazyDeploymentCompletedGetter: z.boolean().optional(),
/**
* If set to true, disables generation of test files.
*/
skipTestGeneration: z.boolean().optional(),
});

export const modeSchema: z.ZodType<C.Mode> = z.union([
Expand Down
5 changes: 5 additions & 0 deletions src/config/configSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
"type": "boolean",
"default": false,
"description": "False by default. If set to true, enables generation of `lazy_deployment_completed()` getter. Does nothing if contract parameters are declared."
},
"skipTestGeneration": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, disables generation of test files."
}
}
},
Expand Down
Loading
Loading