Skip to main content

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.

Tests are 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 command-line interpreter:

rell.sh -d my_src_directory --test my_test_module

or right-click 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() - equivalent 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 of a Test Module

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

Running tests in docker

If one is running their program from a docker instance, testing will be slightly different. Tests will run through a script that will run the tests specified in the test module.

note

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

Implementing docker test functionality into an already existing project means pasting this script manually into your project folder. Alongside the script, a docker compose file that specifies from where and how the tests will run will also be needed.

The docker-compose.yaml file will look 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 writing the following inside the folder rell-tests.js is inside:

node rell-tests.js