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

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.