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 keypairsrell.test.privkeys.{bob, alice, trudy, charlie, dave, eve, frank, grace, heidi}: byte_array
- same asrell.test.keypairs.X.priv
rell.test.pubkeys.{bob, alice, trudy, charlie, dave, eve, frank, grace, heidi}: byte_array
- same asrell.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 userell.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 anop
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 builderrell.test.block(tx: rell.test.tx, ...)
- create a block builder with some transactionsrell.test.block(txs: list<rell.test.tx>)
- same asrell.test.block(tx: rell.test.tx, ...)
rell.test.block(op: rell.test.op, ...)
- create a block builder with one transaction with some operationsrell.test.block(ops: list<rell.test.op>)
- same asrell.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. Whenexpected
is specifiedrun_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 builderrell.test.tx(op: rell.test.op, ...)
- create a transaction builder with some operationsrell.test.tx(ops: list<rell.test.op>)
- same asrell.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. Whenexpected
is specifiedrun_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 argumentsrell.test.op(name: text, args: list<gtv>)
- same asrell.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. Whenexpected
is specifiedrun_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 equalassert_not_equals(actual: T, expected: T)
- fail if the values are equalassert_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 nullassert_not_null(actual: T?)
- assert that the value isn't nullassert_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 valuef
does not fail. The passed function can fail with any kind of error except a test assertion error. If the passed function fails andexpected
is specifiedassert_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, *)); // FailsUse 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.
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