Skip to main content

Test Rell code

To write unit tests for Rell code, use test module. You need to use the @test annotation to define a test module.

@test module;

function test_foo() {
assert_equals(2 + 2, 4);
}

function test_bar() {
assert_equals(2 + 2, 5);
}

All functions in a test module that start with test_ (and a function called exactly test) are test functions that're executed when you run the test module.

If the module has the "_test" suffix, it becomes a test module for the module that bears the same name without the suffix. For example, if the module name is program, the test module is program_test.

💡 Best practice
▶️ Follow clear and concise test naming conventions

Test names should clearly state what is being tested, not how. This improves readability and makes test reports easier to understand.

  • ✅ Prefix test names with test_.

  • ✅ Focus on business scenarios.

  • 🚫 Avoid redundant words like it_should_ or _successful.

    // 🚫 Unclear and verbose
    test_it_should_get_user
    test_return_paginated_users_list_by_id_successful

    // ✅ Clear and focused
    test_get_full_user_profile_by_id
    test_add_ipfs_link_as_user_profile_avatar

Disabling tests​

The @disabled annotation (since 0.15.1) skips tests. Applied to a test module, it skips every test function in that module and its submodules. Applied to a test function, it skips that function; this works both for functions marked with @test and for test functions named test or starting with test_.

// Disable all tests in this test module
@test @disabled module;

function test_addition() {
// not executed
}
// Disable a single test function in an otherwise active test module
@test module;

@disabled @test function test_edge_case() {
// not executed
}

@test function test_happy_path() {
// executed normally
}

Using @disabled on a non-test module or a non-test function is a compile error.

To run a test module, use the test command (chr test):

chr test --settings chromia.yml --modules my_test_module

For more information, see the chr test topic. Each test function gets executed independently of others, and a summary gets printed at the end:

TEST RESULTS:

my_test_module:test_foo OK
my_test_module:test_bar FAILED

SUMMARY: 1 FAILED / 1 PASSED / 2 TOTAL


***** FAILED *****

Test module (file data_test.rell):​

@test module;
import data;

function test_add_user() {
assert_equals(data.user @* {}(.name), list<text>());

val tx = rell.test.tx(data.add_user('Bob'));
assert_equals(data.user @* {}(.name), list<text>());

tx.run();
assert_equals(data.user @* {}(.name), ['Bob']);
}
💡 Best practice
▶️ Naming and coverage conventions for Rell tests
  • ✅ Use descriptive variable names that clearly reflect user roles or test scenarios.

    // 🚫 Unclear naming
    val (admin_user, _) = owned_tests_utils.create_test_user(admin);
    val (alice_user, _) = owned_tests_utils.create_test_user(alice);

    // ✅ Clear and meaningful naming
    val (admin, _) = owned_tests_utils.create_test_user(admin);
    val (alice, _) = owned_tests_utils.create_test_user(alice);
  • ✅ Ensure comprehensive test coverage for entity variations.

    When testing queries like:

    q @* { q.user == user, q.project == project }

    Make sure to:

    • Test multiple user and project combinations.
    • Vary configurations to cover edge cases.
    • Simulate realistic, business-driven scenarios.

Example of a test module​

Here's an example of a test module:

@test module;
import main;

function test_init(){

assert_equals((main.user @* {}(.username)).size(),0);
assert_equals((main.balance @* {}(.user)).size(),0);
rell.test.tx().op(main.init(rell.test.pubkeys.alice)).run();
assert_equals(main.user @* {}(.username).size(),1);
assert_equals(main.balance@{.user == main.user @ { .pubkey == rell.test.pubkeys.alice}}(.amount),1000000);

}

function test_register_user(){

rell.test.tx().op(main.init(rell.test.pubkeys.alice)).run();
assert_equals(main.user @* {}(.username).size(),1);
rell.test.tx().op(main.register_user(rell.test.pubkeys.alice,rell.test.pubkeys.bob,"bob",100)).sign(rell.test.keypairs.alice).run();
assert_equals(main.user @* {}(.username).size(),2);
assert_equals(main.user @ {.pubkey == rell.test.pubkeys.bob}(.username),"bob");
assert_equals(main.balance@{.user == main.user @ {.pubkey == rell.test.pubkeys.bob}}(.amount),100);
}

function test_blocks(){

val tx1 = rell.test.tx().op(main.init(rell.test.pubkeys.alice));
val tx2 = rell.test.tx().op(main.register_user(rell.test.pubkeys.alice,rell.test.pubkeys.bob,"bob",100)).sign(rell.test.keypairs.alice);
val tx3 = rell.test.tx().op(main.create_channel(rell.test.pubkeys.alice,"channel 1")).sign(rell.test.keypairs.alice);
rell.test.block().tx(tx1).tx(tx2).tx(tx3).run();
val tx4 = rell.test.tx().op(main.add_channel_member(rell.test.pubkeys.alice,"channel 1","bob")).sign(rell.test.keypairs.alice);
rell.test.block().tx(tx4).run();
}

Unit tests also display the duration of each test case and the overall test suite duration.

TEST RESULTS:

OK tests.foobar:test_foo (0.005s)

FAILED tests.foobar:test_fail_require (0.001s)
FAILED tests.foobar:test_fail_assert_equals (0.001s)

SUMMARY: 2 FAILED / 1 PASSED / 3 TOTAL (0.007s)
💡 Best practice

✅ Focus each negative test case on one specific failure scenario for clarity.

✅ Use consistent, descriptive naming conventions for failure tests, e.g function test_internal_transfer_fails.

✅ Use provided helper functions to register users efficiently:

val admin = register_admin();
val alice = register_alice();
val bob = register_bob();

🚫 Avoid verbose setup patterns:

val admin_account = create_ft3_test_acc(admin_keypair());

✅ When using run_must_fail(), always include an expected error message to make failure reasons explicit:

// Without error message – failure reason unclear
owned.credits_external.create_credit_role("Role")
.sign(admin.keypair)
.run_must_fail();

// With error message – failure reason clear
owned.credits_external.create_credit_role("Role")
.sign(admin.keypair)
.run_must_fail("Failed to create credit role that already exists");

Testing framework overview​

The Rell testing framework provides tools for creating and executing test blocks, transactions, and operations. For complete API documentation, see the rell.test namespace reference.

Test builders​

Test code uses builder types to construct blocks, transactions, and operations:

  • rell.test.block – Builder for test blocks containing transactions
  • rell.test.tx – Builder for test transactions containing operations
  • rell.test.op – Represents a test operation call (created by calling any operation)

Builders follow a fluent pattern and support method chaining. All three support .run() to execute immediately and .run_must_fail() to assert failure.

Assertions​

The rell.test namespace provides assertion functions for verifying test expectations:

Test keys​

Predefined test keypairs are available for signing test transactions. These should never be used in production:

Available names: alice, bob, charlie, dave, eve, frank, grace, heidi, trudy.

Block timestamps​

Test block timestamps are deterministic and controlled through:

Other utilities​

  • rell.test.nop – Create a no-op operation (useful for making transactions unique)
  • rell.test.get_events – Retrieve all events emitted during the last block construction
tip

For information on writing integration tests using Rell and TypeScript, visit the Chromia learning platform.