Motoko unit testing

I’m currently developing a canister and have been writing unit tests using asserts and whatnot, but is there a way to simulate calls to a canister from a particular principal?
The use case for this is so I can test permissions to call particular functions from different principals.
I’ve seen some examples using shell scripts, but this requires deploying the canister and is pretty slow to iterate on.

Any good solutions?

4 Likes

Check out GitHub - chenyan2002/ic-repl.

Not sure if you can change the calling principal in ic-repl scripting language, but worst case you can change your dfx identity in bash and somehow string it together.

I’m currently using a shell script to change dfx identity, but when I want to update something, it takes a couple of minutes to re-deploy canisters which isn’t ideal. I guess I can run the canister and then edit a shell script separately, but then I will have to use the same replica?

In ic-repl, you can use the identity command to switch caller identities. For example

identity alice;  // generates a random principal
call canister_id.method(); // caller is alice
identity bob "my_pem_file.pem";  // assign identity from "my_pem_file.pem" to bob
call canister_id.method(); // caller is bob
identity alice; // switch identity back to alice

Canister calls go through consensus, so it’s not fast. To test things end-to-end, we unfortunately have to tolerant this delay. For function level test, you can try to use matcher (GitHub - kritzcreek/motoko-matchers), which doesn’t require canister calls, so it’s much faster.

1 Like

Ah okay, interesting, thanks!

How do I install ic-repl cli?

You can download from the release page: Releases · chenyan2002/ic-repl · GitHub

1 Like

Got it! How do we obtain a .wasm file from a canister motoko file? I see you use a greet.wasm it in one of the examples.

After you run dfx build or dfx deploy, the Wasm file is at .dfx/local/canisters/

Oh right, thanks! Are you able to enable debug messages with ic-repl? Also, do you know if there’s a docker image for ic-repl?

I’ve just written up a blog post on how to get up and running with Motoko unit tests.

You can check out the reference code here: https://github.com/krpeacock/motoko-unit-tests

4 Likes

@kpeacock why would you use this over the motoko-matchers library that already exists and is represented in the dfinity vessel-package-set?

I found the motoko-lilbrary-template to be a great example of how to set up tests.

The only thing that’s currently missing are tests for actor functions, which are blocked by this wasmtime issue, as wasmtime will complain about any async function tests. What I’ve done so far to get around this is extract as much logic outside of the actor as possible. Still, it would be nice to be able to test my actor without manually deploying and testing or running a full integration test.

The ActorSpec simply pattern feels more familiar to me, because I’ve done most of my test writing in Jest. I set up ActorSpec so I could skip test cases that aren’t ready yet, and I also like that I can run my full set of unit tests and get the results of all of them, even if some are failing. There’s nothing wrong with using the matchers library, though

1 Like

Totally can see where you’re coming from in the case of the test patterns being different from Jest.

Can you elaborate what you mean by skip test cases that aren’t ready yet? The matchers library will run all the tests and get the results (i.e. not short circuit on first failure).

From a quick run where I altered two of my tests to now fail, they will both display as failing, but the rest will succeed (although I don’t get a nice “x/n passed” type of message currently)

% make test
make -C test
[INFO] vessel.dhall
[INFO] vessel.dhall
[INFO] Installing 2 packages
[INFO] Installation complete.
.vessel/.bin/0.6.20/moc --package base .vessel/base/e0c95f909e17c431921f1be35800242a6ddd4a55/src --package matchers .vessel/matchers/v1.1.0/src -wasi-system-api -o WriterTest.wasm WriterTest.mo && wasmtime WriterTest.wasm
Running Writer features tests...

Writer features/after initialization, read() defaults the empty string with white textColor failed:
"" was expected to be "g"

Writer features/initialization provides an escape sequence with transparent color failed:
"\1b[37m\1b[0m" was expected to be "\1b[37mg\1b[0m"

2 tests failed.
assertion failed at Suite.mo:93.13-93.26
Error: failed to run main module `WriterTest.wasm`

Caused by:
    0: failed to invoke command default
    1: wasm trap: wasm `unreachable` instruction executed
       wasm backtrace:
           0:  0xbda - <unknown>!run
           1:  0x89c - <unknown>!init
           2: 0x2832 - <unknown>!_start
       
make[1]: *** [default] Error 134
make: *** [test] Error 2

Ah, I take that back - it will short circuit if you have multiple suites of tests. Good point

That can be worked around by using the Motoko interpreter to run tests, although there is a difference to the environment in which the code is running using that approach.

The works with ActorSpec at least, I’m not sure about with the matchers package.

1 Like

@kpeacock in response to your concern here:

“I also like that I can run my full set of unit tests and get the results of all of them, even if some are failing”

I was just playing around with this this today, and if you look at suite() function definition in motoko-matchers, you can actually calls to suite within one another.

This means that you make a testing pattern like

let clazz1Suite = suite(...);
let clazz2Suite = suite(...);
let clazz3Suite = suite(...);

run(suite("all tests", [
  clazz1Suite,
  clazz2Suite,
  clazz3Suite
]));

Additionally, if you wanted to have these tests in multiple files, you would do the following.

// TestClazz1.mo
// all imports

module {
  public let clazz1Suite = suite(...)
}

Repeat the above over all your class files, then…

// RunTests.mo
import TestClazz1 "./TestClazz1";
import TestClazz2 "./TestClazz2";
import TestClazz3 "./TestClazz3";
// other imports

run(suite("all tests", [
  TestClazz1.clazz1Suite,
  TestClazz2.clazz2Suite,
  TestClazz3.clazz3Suite
]));

I’d imagine that this could be automated further (removing the need to wrap each file test in a module and the import step) by having some sort of bash script loop

for file in *.mo; do \
  ...run each file, write errors to a file

then have another script (node/python) grab this and pretty print it.

@kritzcreek curious if you had any original intentions of how to build this out instead of the current exit(1) on failures within run(). I’d be interested in making a few contributions, but want to vet the idea before starting work/opening a PR, etc.

3 Likes