Testing module

To write unit tests for Rell code, use test modules. A test module is defined using the @test annotation:

@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 and will be executed when the test module is run.

To run a test module, use the command-line interpreter:

rell.sh -d my_src_directory --test my_test_module

or right-klick on the run.xml file and run as Rell Unit Test

Each test function will be executed independently of others, and a summary will be printed in the end:

TEST RESULTS:

my_test_module:test_foo OK
my_test_module:test_bar FAILED

SUMMARY: 1 FAILED / 1 PASSED / 2 TOTAL


***** FAILED *****

Transactions in test module

Instead of writing tests in a frontend, we write the transactions in rell like this:

rell.test.block - a block, contains a list of transactions

rell.test.tx - a transaction, has a list of operations and a list of signers

rell.test.op - an operation call, which is a (mount) name and a list of arguments (each argument is a gtv)

Keys used for signing transactions can also be created like this:

rell.test.keypairs.{bob, alice, trudy}: rell.test.keypair - test keypairs rell.test.privkeys.{bob, alice, trudy}: byte_array - same as rell.test.keypairs.X.priv rell.test.pubkeys.{bob, alice, trudy}: byte_array - same as rell.test.keypairs.X.pub

Example of a operation signed with the alice keypair:

rell.test.tx().op(main.exampleOp(parameter1,parameter2)).sign(rell.test.keypairs.alice).run();

And if if nop is necessary for making a transaction unique, one can use ´ŕell.test.nop() to implement it.

rell.test.nop(x: integer): rell.test.op rell.test.nop(x: text): rell.test.op rell.test.nop(x: byte_array): rell.test.op

Creates a “nop” operation with a specific argument value.

Building and running a block

operation foo(x: integer) { ... }
operation bar(s: text) { ... }

...

val tx1 = rell.test.tx()
    .op(foo(123))                           // operation call returns rell.test.op
    .op(bar('ABC'))                         // now the transaction has two operations
    .sign(rell.test.keypairs.bob)           // signing with the "Bob" test keypair
    ;

val tx2 = rell.test.tx()
    .op(bar('XYZ'))
    .sign(rell.test.keypairs.bob)
    .sign(rell.test.keypairs.alice)         // tx2 is signed with both "Bob" and "Alice" keypairs
    ;

rell.test.block()
    .tx(tx1)
    .tx(tx2)
    .run()                                  // execute the block consisting of two transactions: tx1 and tx2
    ;

If we our module has the “_test” suffix it will become a test module for the module that bears the same name without the suffix. For example, if our modules name is program, the test module would be called program_test.

Production and test modules

Production module (file data.rell):

module;

entity user {
    name;
}

operation add_user(name) {
    create user(name);
}

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']);
}

Functions of rell.test.block

rell.test.block() - create an empty block builder
rell.test.block(tx: rell.test.tx, ...) - create a block builder with some transaction(s)
rell.test.block(txs: list<rell.test.tx>) - same
rell.test.block(op: rell.test.op, ...) - create a block builder with one transaction with some operation(s)
rell.test.block(ops: list<rell.test.op>) - same
.tx(tx: rell.test.tx, ...) - add some transaction(s) to the block
.tx(txs: list<rell.test.tx>) - same
.tx(op: rell.test.op, ...) - add one transaction with some operation(s) to the block
.tx(ops: list<rell.test.op>) - same
.copy(): rell.test.block - returns a copy of this block builder object
.run() - run the block
.run_must_fail() - same as .run(), but throws exception on success, not on failure

Functions of rell.test.tx:

rell.test.tx() - create an empty transaction builder
rell.test.tx(op: rell.test.op, ...) - create a transaction builder with some operation(s)
rell.test.tx(ops: list<rell.test.op>) - same
.op(op: rell.test.op, ...) - add some operation(s) to this transaction builder
.op(ops: list<rell.test.op>) - same
.nop() - same as .op(rell.test.nop())
.nop(x: integer) - same as .op(rell.test.nop(x))
.nop(x: text) - same
.nop(x: byte_array) - same
.sign(keypair: rell.test.keypair, ...) - add some signer keypair(s)
.sign(keypairs: list<rell.test.keypair>) - same
.sign(privkey: byte_array, ...) - add some signer private key(s) (a private key must be 32 bytes)
.sign(privkeys: list<byte_arrays>) - same
.copy(): rell.test.tx - returns a copy of this transaction builder object
.run() - runs a block containing this single transaction
.run_must_fail() - same as .run(), but throws exception on success, not on failure

Functions of rell.test.op:

rell.test.op(name: text, arg: gtv, ...) - creates an operation call object with a given name and arguments
rell.test.op(name: text, args: list<gtv>) - same
.tx(): rell.test.tx - creates a transaction builder object containing this operation
.sign(...): rell.test.tx - equivalent of .tx().sign(…)
.run() - equivalent of .tx().run()
.run_must_fail() - eqiovalent of .tx().run_must_fail()

Functions assert_* for unit tests

Other functions:

assert_equals(actual: T, expected: T) - fail (throw an exception) if two values are not equal
assert_not_equals(actual: T, expected: T) - fail if the values are equal
assert_true(actual: boolean) - assert that the value is “true”
assert_false(actual: boolean) - assert that the value is “false”
assert_null(actual: T?) - assert that the value is null
assert_not_null(actual: T?) - assert that the value is not null
assert_lt(actual: T, expected: T) - assert less than (actual < expected)
assert_gt(actual: T, expected: T) - assert greater than (actual > expected)
assert_le(actual: T, expected: T) - assert less or equal (actual <= expected)
assert_ge(actual: T, expected: T) - assert greater or equal (actual >= expected)
assert_gt_lt(actual: T, min: T, max: T) - assert (actual > min) and (actual < max)
assert_gt_le(actual: T, min: T, max: T) - assert (actual > min) and (actual <= max)
assert_ge_lt(actual: T, min: T, max: T) - assert (actual >= min) and (actual < max)
assert_ge_le(actual: T, min: T, max: T) - assert (actual >= min) and (actual <= max)

Same functions are also available in the rell.test namespace.

Running unit tests via run.xml

With the help of run.xml we can run tests with module arguments included(constants we define in the run xml file). To define a test in xml, we will use the test tag like this:

<run>
    <nodes>
        <config src="node-config.properties" />
        <test-config src="node-config-test.properties" />
    </nodes>

    <chains>
        <chain name="foo" iid="1">
            <config height="0">
                <app module="foo.app" />
            </config>
            <test module="foo.tests" />
        </chain>

        <test module="lib.tests" />
    </chains>
</run>

We will run the test like this in the terminal: ./multirun.sh -d rell/src --test rell/config/run.xml

Example

Here is an example of a test module implemented for the Chroma-Chat example that can be found in the Example Projects section.

@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();
}