New tests use new tvm op codes, which are not available in the stable version of TON, so you need to install the dev version of func/fift. You need to compile func/fift binaries from toncli-local branch of ton from here. (see INSTALLATION.md) You also need to make sure you use new versions of Asm.fif and AsmTests.fif from this repository.
Note: if you try to run tests written in an old way with a new toncli version, you will probably see an error
like PROC:<{:procedure already defined
. You need to either rewrite tests in a new way (which is preferred!) or pass --old
flags as argument of toncli run_tests
.
- No need to manually specify method_ids for test functions.
- No need to hardcode function selectors of contract's functions.
- No need to split the test into "prepare" and "check" code.
- You can invoke more than one contract's method in each test.
- If one test corrupts the stack in some way, other tests will not be affected.
- Easily write tests which require sending messages signed with private keys.
- Each test function name should start with "
__test
" (e.g.int __test_example() {
). This way we can determine which functions are tests, and which functions are just helpers. - Every time you call a contract's method, you need to use
invoke_method
(which assumes code will not throw) orinvoke_method_expect_fail
(which assumes code will throw) functions. They catch exceptions and compute gas usage. - From each test you can return any number of values, they will be added to the printed report. For example, you can return consumed gas there.
A lot of tests written in a new way could be found in nft_collection
and nft_item
projects examples. Also, a lot of helpers in tests-libs
can be founded.
Assume we have a contract, which stores the 64-bit integer inside, increases it when receiving an internal message, and reports current value on get_total
.
We can write a test like this:
int __test_example() {
set_data(begin_cell().store_uint(0, 64).end_cell());
cell message = begin_cell().store_uint(10, 32).end_cell();
var (int gas_used1, _) = invoke_method(recv_internal, [message.begin_parse()]);
var (int gas_used2, stack) = invoke_method(get_total, []);
[int total] = stack;
throw_if(101, total != 10);
return gas_used1 + gas_used2;
}
invoke_method
takes two arguments: a method name, and arguments (which will be passed to the method) as a tuple. It returns two values: used gas,
and values returned from the method (as tuple). If method throws, invoke_method
will also throw, and the test will fail.
If you expect a method to throw, you should use invoke_method_expect_fail
:
int __test_no_initial_data_should_fail() {
int gas_used = invoke_method_expect_fail(get_total, []);
return gas_used;
}
It takes two arguments in the same way as invoke_method
, but returns only the amount of gas used by the method.
You can use set_now(12345)
inside the test to make now
equal to 12345.
If you expect method to generate some actions (e.g. via send_raw_message
), after invoke_method
you can use get_actions()
to get generated c5
.
There is also a special function assert_no_actions()
, which you can use to check that no actions were generated by the method.
Actions are cleared before each invoke_method
, so you need to check them after each invoke_method
call, not at the end of the whole test.
There are some helper functions, which could be used for cryptography. You can find specific examples here.
You can use get_prev_c4()
to get persistent data of the last successful test. But personally I'd discourage people from using it.
It is better to create a helper function, which sends all needed queries to the contract to change persistent data as required,
and use it from different tests.