Skip to main content

Testing

danger

Legacy Cairo 0 test runner has been deprecated, and will be removed in future releases. Consider migrating your tests to Cairo 1 test runner. You can still test your Cairo 0 contracts using the new Cairo 1 test runner.

info

This command has been recently renamed to test-cairo0.

Protostar provides a flexible testing environment for Cairo smart contracts. It allows to write unit/integration tests with a help of cheatcodes.

Unit testing

We will start with a just created protostar project. In your src directory create a utils.cairo file

src/utils.cairo
func sum_func{syscall_ptr: felt*, range_check_ptr}(a: felt, b: felt) -> felt {
return a + b;
}

This is our target function, which we are going to test. Then in the tests directory create file test_utils.cairo, which contains a single test case.

tests/test_utils.cairo
%lang starknet

from src.utils import sum_func

@external
func test_sum{syscall_ptr: felt*, range_check_ptr}() {
let r = sum_func(4, 3);
assert r = 7;
return ();
}

Then run your test with

protostar test-cairo0 ./tests
info

In the example above, Protostar will run every test case it manages to find in the tests directory. You can read more about specifying where and how Protostar should search for test cases by running protostar test-cairo0 --help.

tip

If you experience any errors during test collection phase consider using --safe-collecting flag.

Expected result
Collected 1 items

test_utils: .
----- TEST SUMMARY ------
1 passed
Ran 1 out of 1 total tests
info

You can place your test files anywhere you want. Protostar recursively searches the given directory for Cairo files with a name starting with test_ and treats them as tests files. All functions inside a test file starting with test_ are treated as separate test cases.

danger

Protostar auto-removes constructors from test files. You can test a constructor using the deploy_contract cheatcode.

note

If you need to print machine-readable output in JSON format, you should use --json flag.

This may come in handy for writing scripts that include protostar commands.

For more information, go to this page

Asserts

Protostar ships with its own assert functions. They don't accept implicit arguments compared to asserts from starkware.cairo.common.math. You can import Protostar asserts in the following way:

test_my_contract.cairo
from protostar.asserts import (
assert_eq,
assert_not_eq,
assert_signed_lt,
assert_signed_le,
assert_signed_gt,
assert_unsigned_lt,
assert_unsigned_le,
assert_unsigned_gt,
assert_signed_ge,
assert_unsigned_ge,
)
info

If your IDE supports Cairo and doesn't know how to import protostar, add the following directory $(which protostar)/../cairo to the CAIRO_PATH.

You can find all assert signatures here.

Setup hooks

Often while writing tests you have some setup work that needs to happen before tests run. The __setup__ (setup suite) and setup_<test_name> (setup case) hooks can simplify and speed up your tests.

Use the context variable to pass data from setup hooks to test functions as demonstrated in examples below.

Setup suite

@external
func __setup__()

The setup suite hook is shared between all test cases in a test suite (Cairo module), and is executed before test cases.

Using setup suite hook
@external
func __setup__() {
%{ context.contract_a_address = deploy_contract("./tests/integration/testing_hooks/basic_contract.cairo").contract_address %}
return ();
}

@external
func test_something() {
tempvar contract_address;
%{ ids.contract_address = context.contract_a_address %}

// ...

return ();
}
info

Protostar executes __setup__ only once per test suite. Then, for each test case Protostar copies the Starknet state and the context object.

Setup case

@external
func setup_tested_property()

@external
func test_tested_property()

The setup case hook is bound to a matching test case and is executed just before the test case itself. The hook is executed within a context built by the __setup__ hook, but it does not influence other test cases' contexts. Then, Protostar immediately executes the test case function. This makes them useful to extract test-specific setup logic from test code.

Using setup case hook to prepare test-specific state
@external
func __setup__{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
balance.write(10);
return ();
}

@external
func setup_need_more_money{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
balance.write(10000);
return ();
}

@external
func test_need_more_money{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
alloc_locals;
let (amount_ref) = balance.read();
local amount = amount_ref;

assert amount = 10000;

return ();
}

@external
func test_foo{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
alloc_locals;
let (amount_ref) = balance.read();
local amount = amount_ref;

assert amount = 10;

return ();
}

You can also use setup case hooks to configure the behavior of a particular test case, for example, by calling the max_examples cheatcode. Some configuration-specific cheatcodes are only available within setup cases, like example and given:

Using setup case hook to configure fuzzing test
@external
func setup_something() {
%{
max_examples(500)
given(a = strategy.felts())
%}
return ();
}

@external
func test_something(a: felt) {
// ...

return ();
}

Importing Python modules in hints

Protostar allows using external Python code in hint blocks, for example to verify a signature using third party library.

The cairo-path is automatically added to sys.path in executed hints. This includes project root, src and lib directories. Any Python module files stored there can be imported without any extra configuration.

The PYTHONPATH environment variable is fully supported, and Protostar will extend sys.path with this variable's value in executed Cairo code. This approach can be used to include some packages from Python virtual environment (by adding site_packages to the PYTHONPATH).

For example, having the standard project file structure:

.
├── lib
├── protostar.toml
├── src
│ └── main.cairo
└── tests
├── pymodule.py
└── test_main.cairo

In pymodule.py:

def get_three():
return 3

The get_three function can be used in test_main.cairo like this:

%lang starknet
from src.main import balance, increase_balance
from starkware.cairo.common.cairo_builtins import HashBuiltin

@external
func test_getting_tree() {
alloc_locals;
local res;
%{
from tests.pymodule import get_three
ids.res = get_three()
%}

assert res = 3;
return ();
}