Writing Tests Examples
This page demonstrates how to write test for FunC contracts created in with Blueprint SDK (Sandbox).
Test suites built for demo contract fireworks. The fireworks is a smart contract which initially run via set_first
message.
Once a new FunC project is created via npm create ton@latest
, a test file tests/contract.spec.ts
will be autogenerated 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
});
Running tests using the following command:
npx blueprint test
Additional options and vmLogs may be specified with blockchain.verbosity
:
blockchain.verbosity = {
...blockchain.verbosity,
blockchainLogs: true,
vmLogs: 'vm_logs_full',
debugLogs: true,
print: false,
}
Direct Unit Tests
Fireworks demonstrate different operating with sending messages in the TON Blockchain.
Once you deploy this with message set_first
with enough TON amount, it will be automatically executed with primary and usable combinations of send modes.
Fireworks redeployed itself, as result it will be created 3 entities of Fireworks entities, while each of entity has own ID(keep it in storage) and, as a result, different Smart Contract Address.
For clearness define different by ID Fireworks instances (different state_init
) with the following names:
- 1 - Fireworks setter - The entity that spread different launch op codes. Could be extended up to four different opcodes.
- 2 - Fireworks launcher-1 - The Fireworks instance, which launch first fireworks, means messages will be sent to the launcher.
- 3 - Fireworks launcher-2 - The Fireworks instance, which launch second fireworks, means messages will be sent launcher.
Expand details on transactions
index - is an ID of a transaction in the launchResult
array.
0
- External request to the treasury (the Launcher) that resulted with a outbound messageop::set_first
with 2.5 to fireworks1
- The transaction in Fireworks setter contract invoked withop::set_first
and executed with two outbound messages to the Fireworks Launcher-1 and Fireworks Launcher-22
- The transaction in the Fireworks launcher 1 invoked withop::launch_first
, and executed with four outbound messages to the Launcher.3
- The transaction in the Fireworks launcher 2 invoked withop::launch_second
, and executed with a outbound message to the Launcher.4
- Transaction in the Launcher with incoming message from the Fireworks launcher 1. This message sent withsend mode = 0
.5
- Transaction in the Launcher with incoming message from the Fireworks launcher 1. This message sent withsend mode = 1
6
- Transaction in the Launcher with incoming message from the Fireworks launcher 1. This message sent withsend mode = 2
7
- Transaction in the Launcher with incoming message from the Fireworks launcher 1. This message sent withsend mode = 128 + 32
8
- Transaction in the Launcher with incoming message from the Fireworks launcher 2. This message sent withsend mode = 64
Each 'firework' - is outbound message with a unique message body appears in transactions with ID:3 and ID:4.
Bellow the list of test for each transaction expected successfully executed. Transaction[ID:0] External request to the treasury (the Launcher) that resulted with a outbound message op::set_first
with 2.5 to fireworks. In case you will deploy Fireworks to the blockchain launcher is your wallet.
Transaction ID:1 Success Test
This test checks if the fireworks are successfully set by sending a transaction with a value of 2.5 TON. This is the simplest case, the main purpose here to assert result of transaction success property to true.
To filter certain transaction from the launhcResult.transactions
array, we can use the most convince fields.
With
from
(contract sender address), to
(contract destination address), op
(Op code value) - we will retrieve only one transaction for this combination.
The transaction[ID:1] in Fireworks Setter contract invoked with op::set_first
and executed with two outbound messages to the 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 the transaction[ID:2] executed successfully.
The transaction in the Fireworks launcher 1 invoked with op::launch_first
, and executed with four outbound messages to the Launcher.
it('should exist a transaction[ID:2] which launch 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);
});
In cases, when transaction should affect the state of contract, it is possible to specify this with destroyed
, endStatus
fields.
The full list of Account Status related fields:
destroyed
-true
- if the existing contract was destroyed due to executing a certain transaction. Otherwise -false
.deploy
- Custom Sandbox flag that indicates whether the contract was deployed during this transaction.true
if contract before this transaction was not initialized and after this transaction became initialized. Otherwise -false
.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 the transaction[ID:3] executed successfully.
The transaction[ID:3] carries out in the Fireworks launcher 1, invokes with op::launch_first
, and executes with four outbound messages to the Launcher.
it('should exist a transaction[ID:3] which launch 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 the transaction[ID:4] executed successfully.
Transaction[ID:4] carries out in the Launcher(Deploy Wallet) with incoming message from the Fireworks launcher 1. This message 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 the transaction[ID:5] executed successfully.
Transaction[ID:5] carries out in the Launcher with incoming message from the Fireworks launcher 1. This message 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 the transaction[ID:6] executed successfully.
The transaction[ID:6] carries out in the Launcher with incoming message from the Fireworks launcher 1. This message 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 the transaction[ID:7] executed successfully.
The transaction[ID:7] carries out in the Launcher with incoming message from the Fireworks launcher 1. This message 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 the transaction[ID:8] executed successfully.
The transaction[ID:8] carries out in the Launcher with incoming message from the Fireworks launcher 2. This message 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
During the test, reading the details about fees can be useful for optimizing the contract. The printTransactionFees function prints the entire transaction chain in a convenient manner."
it('should be executed 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 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 - is an ID of a transaction in the launchResult
array.
0
- External request to the treasury (the Launcher) that resulted in a messageop::set_first
to Fireworks1
- The Fireworks transaction that resulted in 4 messages to the Launcher2
- Transaction on Launched Fireworks - 1 from the Launcher, message sent withop::launch_first
op code.2
- Transaction on Launched Fireworks - 2 from the Launcher, message sent withop::launch_second
op code.4
- Transaction on Launcher with incoming message from the Launched Fireworks - 1, message sent withsend mode = 0
5
- Transaction on Launcher with incoming message from the Launched Fireworks - 1, message sent withsend mode = 1
6
- Transaction on Launcher with incoming message from the Launched Fireworks - 1, message sent withsend mode = 2
7
- Transaction on Launcher with incoming message from the Launched Fireworks - 1, message sent withsend mode = 128 + 32
8
- Transaction on Launcher with incoming message from the Launched Fireworks - 2, message sent withsend mode = 64
Transaction Fees Tests
This test verifies whether the transaction fees for launching the fireworks are as expected. It is possible to define custom assertions for different parts of commission fees.
it('should be executed with expected fees', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
//totalFee
console.log('total fees = ', launchResult.transactions[1].totalFees);
const tx1 = launchResult.transactions[1];
if (tx1.description.type !== 'generic') {
throw new Error('Generic transaction expected');
}
//computeFee
const computeFee = tx1.description.computePhase.type === 'vm' ? tx1.description.computePhase.gasFees : undefined;
console.log('computeFee = ', computeFee);
//actionFee
const actionFee = tx1.description.actionPhase?.totalActionFees;
console.log('actionFee = ', actionFee);
if ((computeFee == null || undefined) ||
(actionFee == null || undefined)) {
throw new Error('undefined fees');
}
//The check, if Compute Phase and Action Phase fees exceed 1 TON
expect(computeFee + actionFee).toBeLessThan(toNano('1'));
});
Edge Cases Tests
In this section will be provided the test cases for TVM exit codes that can occur during transaction processing. These exit codes are in the blockchain code itself. At the same time, it is necessary to distinguish the exit code during the Compute Phase and the exit code during the Action Phase.
During the Compute Phase, the contract logic (its code) is executed. While processing, various actions can be created. These actions will be processed in the next phase - Action Phase. If the Compute Phase is unsuccessful, then the Action Phase does not start. However, if the Compute Phase was successful, this does not guarantee that the Action Phase will also end successfully.
Compute Phase | exit code = 0
This exit code means that the Compute Phase of the transaction was completed successfully.
Compute Phase | exit code = 1
A alternative exit code for denoting the success of the Compute Phase is 1
. To get this exit code, you need to use the RETALT.
It should be noted that this opcode should be called in the main function (for example, recv_internal). If you call in another function, then 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, they appear on the stack. If suddenly there are no elements on the stack, but some opcode requires them, then this error is thrown.
This can happen when working with opcodes directly, since stdlib.fc (a library for FunC) assumes that there will be no such problem.
Compute Phase | exit code = 3
Any code before execution becomes a continuation
. This is a special data type that contains a slice with code, a stack, registers, and other data necessary for the execution of the code. Such a continuation can be run later if necessary, passing the necessary parameters for the initial state of the stack.
First, we build such a continuation. In this case, this is just an empty continuation that does nothing. Next, using the opcode 0 SETNUMARGS
, we indicate that there should be no values in the stack at the beginning of execution. Then, using the opcode 1 -1 SETCONTARGS
, we call the continuation, passing 1 value. Since there should have been no values, we get a StackOverflow error.
Compute Phase | exit code = 4
In TVM, integer
can be in the range -2256 < x < 2256. If the value during the calculation went beyond this range, then 4 exit code is thrown.
Compute Phase | exit code = 5
If the integer
value went beyond the expected range, then 5 exit code is thrown. For example, if a negative value was used in the .store_uint()
function.
Compute Phase | exit code = 6
On a lower level, instead of the familiar functions names, opcodes are used, which can be seen in this table in HEX form. In this example, we use @addop
, which adds a non-existent opcode.
The emulator, when trying to process this opcode, does not recognize it and throws 6.
Compute Phase | exit code = 7
It is a quite common error that occurs when receiving the wrong data type. In example is the case when the tuple
contained 3 elements, but when unpacking there was an attempt to get 4.
There are many other cases when this error is thrown. Some of them:
- not a null
- not an integer
- not a cell
- not a cell builder
- not a cell slice
- not a string
- not a bytes chunk
- not a continuation
- not a box
- not a tuple
- not an atom
Compute Phase | exit code = 8
All data in TON is stored in cells. A cell has the capacity to store 1023 bits of data and 4 references to other cells. If you try to write more than 1023 bits or more than 4 references, 8 exit code is thrown.
Compute Phase | exit code = 9
If you try to read more data from a slice (when reading data from a cell, it must be converted to a slice data type) than it contains, then 9 exit code is thrown. For example, if there were 10 bits in the slice, and 11 were read, or if there were no links to other references, but there was an attempt to load a reference.
Compute Phase | exit code = 10
This error is thrown when working with dictionaries. As an example, the case when the value that belongs to the key is stored in another cell as a reference. In this case, you need to use the .udict_get_ref()
function to get such a value.
However, the link to another cell should be only 1 and not 2, as in our example:
root_cell
├── key
│ ├── value
│ └── value - second reference for one key
└── key
└ ── value
That is why when trying to read the value, we get 10 exit code.
Additional: You can also store the value next to the key in the dictionary:
root_cell
├── key-value
└── key-value
Note: In fact, the structure of the dictionary (how the data is located in the cells) is more complicated than indicated in the examples above. Therefore, they are simplified for understanding the example.
Compute Phase | exit code = 11
This error occurs when something unknown happens. For example, when using the SENDMSG opcode, if you pass the wrong (for example, empty) cell with a message, then such an error will occur.
Also, it occurs when trying to call a non-existent method. Often, developers face this when calling a non-existent GET method.
Compute Phase | exit code = -14 (13)
If there is not enough TON to handle Compute Phase, then this error is thrown. In the enum class Excno
, where the exit codes for various errors in Compute Phase are indicated, the value 13 is indicated.
However, during processing, the NOT operation is applied to this value, which changes this value to -14
. This is done so that this exit code cannot be faked using, for example, the throw
function, since all such functions accept only positive values for the exit code.
Action Phase | exit code = 32
Action Phase begins after Compute Phase and it processes actions that were written to register c5 during Compute Phase. If the data in this register is incorrectly written, then 32 exit code will be thrown.
Action Phase | exit code = 33
At the moment, a maximum of 255
actions can be in one transaction. If this value is exceeded, then the Action Phase will end with 33 exit code.
Action Phase | exit code = 34
This exit code is responsible for most of the errors when working with actions: invalid message, incorrect action, and so on.
Action Phase | exit code = 35
During the building of the CommonMsgInfo part of the message, you must specify the correct source address. It must be equal to either addr_none or the address of the account that sends the message.
In the blockchain code, this is handled by the check_replace_src_addr.
Action Phase | exit code = 36
If the destination address is invalid, then 36 exit code is thrown. Some possible reasons are a non-existent workchain or an incorrect address. All checks can be seen in the check_rewrite_dest_addr.
Action Phase | exit code = 37
This exit code is similar to -14
in Compute Phase. Here it means that there is not enough balance to send the specified amount of TON.
Action Phase | exit code = 38
The same as in 37
exit code, but refers to the lack of ExtraCurrency on the balance.
Action Phase | exit code = 40
In case there is enough TON to process a certain part of the message (let's say 5 cells), and there are 10 cells in the message, 40 exit code is thrown.
Action Phase | exit code = 43
May be occur, if the maximum number of cells in the library is exceeded or the maximum depth of the Merkle tree is exceeded.
Library is a cell that is stored in Masterchain and can be used by all smart contracts if it is public.
Since the order of lines may change when updating the code, some links become irrelevant. Therefore, all links will use the state of the code base at commit 9728bc65b75defe4f9dcaaea0f62a22f198abe96.