Skip to main content

Test module

To write unit tests for Rell code, use test modules. 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.

Tests get executed using a postchain node with the following signer key:

val signer_privkey = x"4242424242424242424242424242424242424242424242424242424242424242";
val signer_pubkey = x"0324653EAC434488002CC06BBFB7F10FE18991E35F9FE4302DBEA6D2353DC0AB1C";

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

chr test --settings config.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 *****

Transactions in a test module

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

  • rell.test.block - a block. It contains a list of transactions.

  • rell.test.tx - a transaction. It 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).

You can create the keys for signing transactions as follows:

  • rell.test.keypairs.{bob, alice, trudy, charlie, dave, eve, frank, grace, heidi}: rell.test.keypair - test keypairs
  • rell.test.privkeys.{bob, alice, trudy, charlie, dave, eve, frank, grace, heidi}: byte_array - same as rell.test.keypairs.X.priv
  • rell.test.pubkeys.{bob, alice, trudy, charlie, dave, eve, frank, grace, heidi}: byte_array - same as rell.test.keypairs.X.pub

Example of an operation signed with the alice keypair:

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

    And if nop is necessary for making a transaction unique, one can use rell.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 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.

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 transactions
  • rell.test.block(txs: list<rell.test.tx>) - same as rell.test.block(tx: rell.test.tx, ...)
  • rell.test.block(op: rell.test.op, ...) - create a block builder with one transaction with some operations
  • rell.test.block(ops: list<rell.test.op>) - same as rell.test.block(op: rell.test.op, ...)
  • .tx(tx: rell.test.tx, ...) - add some transactions to the block
  • .tx(txs: list<rell.test.tx>) - same as .tx(tx: rell.test.tx, ...)
  • .tx(op: rell.test.op, ...) - add one transaction with some operations to the block
  • .tx(ops: list<rell.test.op>) - same as .tx(op: rell.test.op, ...)
  • .copy(): rell.test.block - returns a copy of this block builder object
  • .run() - runs the block
  • .run_must_fail(): rell.test.failure - same as .run(), but gives an exception on success, not on failure. When expected is specified run_must_fail(expected: text): rell.test.failure, the call fails if the actual error message does not contain the expected text.

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 operations
  • rell.test.tx(ops: list<rell.test.op>) - same as rell.test.tx(op: rell.test.op, ...)
  • .op(op: rell.test.op, ...) - add some operations to this transaction builder
  • .op(ops: list<rell.test.op>) - same as .op(op: rell.test.op, ...)
  • .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 keypairs
  • .sign(keypairs: list<rell.test.keypair>) - same as .sign(keypair: rell.test.keypair, ...)
  • .sign(privkey: byte_array, ...) - add some signer private keys (a private key must be 32 bytes)
  • .sign(privkeys: list<byte_arrays>) - same as .sign(privkey: byte_array, ...)
  • .copy(): rell.test.tx - returns a copy of this transaction builder object
  • .run() - runs a block containing this single transaction
  • .run_must_fail(): rell.test.failure - same as .run(), but gives an exception on success, not on failure. When expected is specified run_must_fail(expected: text): rell.test.failure, the call fails if the actual error message does not contain the expected text.

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 as rell.test.op(name: text, arg: gtv, ...)
  • .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(): rell.test.failure - same as .run(), but gives an exception on success, not on failure. When expected is specified run_must_fail(expected: text): rell.test.failure, the call fails if the actual error message does not contain the expected text.

Other functions:

  • assert_equals(actual: T, expected: T) - fail (throw an exception) if two values aren't 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 isn't 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)

  • assert_events(expected: (text,gtv)...) - checks whether the list of events emitted during the last block execution is same as the expected list of events. For example, assert_events(('Foo', (123).to_gtv()), ('Bar', (456).to_gtv()));asserts that the list of events emitted during the last block execution should be the same as the expected list of events, which includes the tuples ('Foo', (123).to_gtv()) and ('Bar', (456).to_gtv()).

  • assert_fails(f: () -> unit): rell.test.failure - fails when the passed function value f does not fail. The passed function can fail with any kind of error except a test assertion error. If the passed function fails and expected is specified assert_fails(expected: text, f: () -> unit): rell.test.failure , the actual error message must contain the expected test.

    Use partial function application to pass a function to assert_fails(). For example:

    function foo(x: integer) {
    require(x >= 0, "x is negative: " + x);
    }
    ...
    assert_fails(foo(-123, *)); // OK
    assert_fails("x is negative: -123", foo(-123, *)); // OK
    assert_fails(foo(123, *)); // Fails

    Use the returned value if non-exact error message matching is needed:

    val f = assert_fails(foo(-123, *));
    assert_true(f.message.starts_with("x is negative: "));
  • rell.test.get_events(): list<(text,gtv)> - returns the list of events emitted during the last block execution.

Running unit tests

You can use the test command (chr test) to run the tests specified in the test key in the project config file (config.yml). For more information, see the chr test topic.

You can also use (chr test) to run all the tests under the test attribute if the current working directory is where the config.yml file exists.

Example of a test module

Here's an example of a test module implemented for the Chroma chat example:

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

Running tests in Docker

Testing is slightly different if one is running their program from a Docker instance. Tests run through a script that runs the tests specified in the test module.

note

For new projects, this project template can get built upon the one that has existing docker files needed and the script that runs tests.

Implementing docker test capability into an existing project means manually pasting this into your project folder. Alongside the script, you need a Docker compose file specifying where and how the tests run.

The docker-compose.yaml file looks something like this:

version: "3.3"
services:
test_postgres:
image: postgres:14.1-alpine
container_name: test_postgres
restart: always
environment:
POSTGRES_DB: postchain
POSTGRES_USER: postchain
POSTGRES_PASSWORD: postchain
test_blockchain:
image: registry.gitlab.com/chromaway/postchain-distribution/chromaway/postchain-test-dapp:3.5.0
container_name: test_blockchain
command:
- ${COMMAND}
depends_on:
- test_postgres
volumes:
- ./rell:/opt/chromaway/rell
environment:
POSTCHAIN_DB_URL: jdbc:postgresql://test_postgres/postchain
CHAIN_CONF: /opt/chromaway/rell/config/run.xml
NODE_CONF: /opt/chromaway/rell/config/node-config.properties

To run your tests, simply run the script by running the following command in the folder where rell-tests.js is located:

node rell-tests.js