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 keypairsrell.test.privkeys.{bob, alice, trudy}: byte_array
- same as rell.test.keypairs.X.privrell.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.
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