Writing test examples
This page demonstrates how to write tests for FunC contracts using the Blueprint (Sandbox).
The test suites focus on the demo contract Fireworks, a smart contract that starts running with the set_first
message.
When you create a new FunC project using npm create ton@latest
, the SDK automatically generates a test file tests/contract.spec.ts
in the project directory for testing the contract:
import ...
describe('Fireworks', () => {
...
expect(deployResult.transactions).toHaveTransaction({
...
});
});
it('should deploy', async () => {
// The check is done inside beforeEach
// blockchain and fireworks are ready to use
});
Run tests using the following command:
npx blueprint test
You can specify additional options and vmLogs using blockchain.verbosity
:
blockchain.verbosity = {
...blockchain.verbosity,
blockchainLogs: true,
vmLogs: "vm_logs_full",
debugLogs: true,
print: false,
};
Direct unit tests
The Fireworks contract demonstrates different ways of sending messages in the TON Blockchain.
When you deploy the contract with the set_first
message and sufficient TON amount,
it automatically executes with primary and usable combinations of send modes.
The Fireworks contract redeploys itself, creating three entities, each with its ID and, as a result, a different smart contract address.
For clarity, define the Fireworks instances with different state_init
by ID with the following names:
- 1 - Fireworks Setter: This entity spreads different launch opcodes and can be extended to support up to four different opcodes.
- 2 - Fireworks Launcher-1: This Fireworks instance launches the first fireworks, sending messages to the launcher.
- 3 - Fireworks Launcher-2: This Fireworks instance launches the second fireworks, sending messages to the launcher.
Expand details on transactions
Index refers to the ID of a transaction in the launchResult
array.
- 0: An external request to the treasury sends an outbound message
op::set_first
with 2.5 TON to Fireworks. - 1: The Fireworks Setter contract processes a transaction with
op::set_first
, sending two outbound messages to Fireworks Launcher-1 and Fireworks Launcher-2. - 2: Fireworks Launcher-1 processes a transaction with
op::launch_first
, sending four outbound messages to the Launcher. - 3: Fireworks Launcher-2 processes a transaction with
op::launch_second
, sending one outbound message to the Launcher. - 4: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 0
. - 5: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 1
. - 6: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 2
. - 7: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 128 + 32
. - 8: The Launcher processes a transaction with an incoming message from Fireworks Launcher-2, sent with
send mode = 64
.
Each "firework" is an outbound message with a unique message body, appearing in transactions with IDs 3 and 4.
Below is a list of tests for each transaction expected to execute successfully.
Transaction ID:1 success test
This test verifies that the fireworks are successfully set by sending a transaction with a value of 2.5 TON.
This is the simplest case, in which the main goal is to confirm that the transaction result's success
property is true
.
To filter a specific transaction from the launchResult.transactions
array, you can use the most convenient fields: from
, to
, and op
. This combination retrieves only one transaction.
The transaction[ID:1] in the Fireworks Setter contract is invoked with op::set_first
and executes two outbound messages to Fireworks Launcher-1 and Fireworks Launcher-2.
it("first transaction[ID:1] should set fireworks successfully", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launcher.address,
to: fireworks.address,
success: true,
op: Opcodes.set_first,
});
});
Transaction ID:2 success test
This test checks if transaction[ID:2] executes successfully.
The transaction in Fireworks Launcher-1 is invoked with op::launch_first
and executes
four outbound messages to the launcher.
it("should exist a transaction[ID:2] which launches first fireworks successfully", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: fireworks.address,
to: launched_f1.address,
success: true,
op: Opcodes.launch_first,
outMessagesCount: 4,
destroyed: true,
endStatus: "non-existing",
});
printTransactionFees(launchResult.transactions);
});
When a transaction affects the state of a contract, you can specify this using the destroyed
and endStatus
fields.
The complete list of account status-related fields includes:
destroyed
:true
if the existing contract was destroyed due to executing a certain transaction. Otherwise, it isfalse
.deploy
: This custom Sandbox flag indicates whether the contract was deployed during this transaction. It istrue
if the contract was not initialized before this transaction and became initialized afterward. Otherwise, it isfalse
.oldStatus
: AccountStatus before transaction execution. Values:'uninitialized'
,'frozen'
,'active'
,'non-existing'
.endStatus
: AccountStatus after transaction execution. Values:'uninitialized'
,'frozen'
,'active'
,'non-existing'
.
Transaction ID:3 success test
This test checks if transaction[ID:3] executes successfully.
The transaction[ID:3] occurs in Fireworks Launcher-1, is invoked with op::launch_first
,
and executes four outbound messages to the launcher.
it("should exist a transaction[ID:3] which launches second fireworks successfully", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: fireworks.address,
to: launched_f2.address,
success: true,
op: Opcodes.launch_second,
outMessagesCount: 1,
});
printTransactionFees(launchResult.transactions);
});
Transaction ID:4 success test
This test checks if transaction[ID:4] executes successfully.
Transaction[ID:4] occurs in the Launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 0
in the transaction[ID:2].
it("should exist a transaction[ID:4] with a comment send mode = 0", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 0")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:5 success test
This test checks if transaction[ID:5] executes successfully.
Transaction[ID:5] occurs in the launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 1
.
it("should exist a transaction[ID:5] with a comment send mode = 1", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 1")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:6 success test
This test checks if transaction[ID:6] executes successfully.
Transaction[ID:6] occurs in the launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 2
.
it("should exist a transaction[ID:6] with a comment send mode = 2", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 2")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:7 success test
This test checks if transaction[ID:7] executes successfully.
Transaction[ID:7] occurs in the launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 128 + 32
.
it("should exist a transaction[ID:7] with a comment send mode = 32 + 128", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 32 + 128")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:8 success test
This test checks if transaction[ID:8] executes successfully.
Transaction[ID:8] occurs in the launcher with an incoming message from Fireworks Launcher-2. This message is sent with send mode = 64
.
it("should exist a transaction[ID:8] with a comment send mode = 64", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f2.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send_mode = 64")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Printing and reading transaction fees
Reading details about fees during testing can help optimize the contract. The printTransactionFees
function prints the entire transaction chain in a convenient format.
it("should execute and print fees", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
console.log(printTransactionFees(launchResult.transactions));
});
For instance, in the case of launchResult
, the following table will be printed:
(index) | op | valueIn | valueOut | totalFees | outActions |
---|---|---|---|---|---|
0 | 'N/A' | 'N/A' | '2.5 TON' | '0.010605 TON' | 1 |
1 | '0x5720cfeb' | '2.5 TON' | '2.185812 TON' | '0.015836 TON' | 2 |
2 | '0x6efe144b' | '1.092906 TON' | '1.081142 TON' | '0.009098 TON' | 4 |
3 | '0xa2e2c2dc' | '1.092906 TON' | '1.088638 TON' | '0.003602 TON' | 1 |
4 | '0x0' | '0.099 TON' | '0 TON' | '0.000309 TON' | 0 |
5 | '0x0' | '0.1 TON' | '0 TON' | '0.000309 TON' | 0 |
6 | '0x0' | '0.099 TON' | '0 TON' | '0.000309 TON' | 0 |
7 | '0x0' | '0.783142 TON' | '0 TON' | '0.000309 TON' | 0 |
8 | '0x0' | '1.088638 TON' | '0 TON' | '0.000309 TON' | 0 |
Index refers to the ID of a transaction in the launchResult
array.
- 0: External request to the treasury that results in a message
op::set_first
to Fireworks. - 1: The Fireworks transaction results in four messages to the launcher.
- 2: Transaction on Launched Fireworks-1 from the Launcher, with a message sent using the
op::launch_first
opcode. - 3: Transaction on Launched Fireworks-2 from the Launcher, with a message sent using the
op::launch_second
opcode. - 4: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 0
. - 5: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 1
. - 6: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 2
. - 7: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 128 + 32
. - 8: Transaction on the Launcher with an incoming message from Launched Fireworks-2, sent with
send mode = 64
.
Transaction fees tests
This test verifies whether the transaction fees for launching the fireworks are as expected. You can define custom assertions for different parts of the commission fees.
it("should execute with expected fees", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
// Total fee
console.log("total fees = ", launchResult.transactions[1].totalFees);
const tx1 = launchResult.transactions[1];
if (tx1.description.type !== "generic") {
throw new Error("Generic transaction expected");
}
// Compute fee
const computeFee =
tx1.description.computePhase.type === "vm"
? tx1.description.computePhase.gasFees
: undefined;
console.log("computeFee = ", computeFee);
// Action fee
const actionFee = tx1.description.actionPhase?.totalActionFees;
console.log("actionFee = ", actionFee);
if (computeFee == null || undefined || actionFee == null || undefined) {
throw new Error("undefined fees");
}
// Check if Compute Phase and Action Phase fees exceed 1 TON
expect(computeFee + actionFee).toBeLessThan(toNano("1"));
});
Edge cases tests
This section provides test cases for TVM exit codes that can occur during transaction processing. These exit codes are part of the blockchain code itself. It is necessary to distinguish between exit codes during the Compute Phase and the Action Phase.
The contract logic is executed during the Compute Phase. Various actions can be created while processing. These actions are processed in the next phase—the Action Phase. If the Compute Phase is unsuccessful, the Action Phase does not start. However, a successful Compute Phase does not guarantee that the Action Phase will also succeed.
Compute Phase | exit code = 0
This exit code indicates that the Compute Phase of the transaction was completed successfully.
Compute Phase | exit code = 1
An alternative exit code for denoting the success of the Compute Phase is 1
. To trigger this exit code, use the RETALT opcode.
This opcode must be called in the main function (e.g., recv_internal
). If called in another function, the exit from that function will be 1
, but the total exit code will be 0
.
Compute Phase | exit code = 2
TVM is a stack machine. When interacting with different values, the system places them on the stack. If an opcode requires elements from the stack but finds it empty, the system throws this error.
This issue can arise when working directly with opcodes, as stdlib.fc assumes this problem will not occur.
Compute Phase | exit code = 3
Before execution, the system converts any code into a continuation
. This special data type includes a slice with code, a stack, registers, and other data required for code execution. If needed, you can run this continuation later, passing the necessary parameters to initialize the stack's state.
First, we build such a continuation. In this case, it is an empty continuation that does nothing. Next, using the opcode 0 SETNUMARGS
, we indicate that no values should be on the stack at the start of execution. Then, we call the continuation using the opcode 1 -1 SETCONTARGS
, passing one value. Since there should have been no values, we get a StackOverflow error.