Motoko unit testing with Mops

I’m excited to announce a new feature in Mops that makes it even easier to run unit tests for your Motoko code.

With the latest update, you can run unit tests with the command mops test in your terminal.
To take advantage of this new feature, you’ll need to update your Mops CLI to the latest version.

When running your tests, Mops will now process the new Mops Message Format, which includes special labels on stdout, like so mops:1:start <test_name> . Mops will then parse and format this output to provide clear and concise test results.

Instruction

  1. Install Mops
npm i ic-mops -g
  1. Install test package
mops add test --dev
  1. Put your tests in test directory in *.test.mo files.

  2. Use test and suite functions from test package in conjunction with assert expression.

  3. Run mops test

It may also be useful to use the fuzz library to generate random data.

Example

Example simple.test.mo:

import {test} "mo:test";

test("simple test", func() {
	assert true;
});

test("test my number", func() {
	assert 1 > 0;
});

mops test output:

Test files:
• test/simple.test.mo
--------------------------------------------------
Running test/simple.test.mo
 ✓ simple test
 ✓ test my number
 PASS
--------------------------------------------------
Tests passed
Done in 0.17s, passed 2

Testing actor classes

You can even test your actor classes with some limitations:

  • You cannot call methods from base/ExperimentalCycles
  • You cannot call other canisters via actor("...")
  • You cannot test preupgrade/postupgrade

More examples:


This new unit test feature allows you to easily test your code and ensures the reliability of your codebase.

So why wait? Try out the mops test command and let me know what you think!

15 Likes

Update overview 04.03.2023

Update CLI to get new features:

npm i ic-mops -g

Filter test files

You can pass glob pattern to filter test files:

mops test <filter>

Example with filter:

$ mops test stor
Test files:
• test/storage.test.mo

Example without filter:

$ mops test
Test files:
• test/fail.test.mo
• test/hello.test.mo
• test/is-letter.test.mo
• test/simple.test.mo
• test/storage.test.mo
• test/validate-license.test.mo

Watch mode

Simply add --watch to re-run tests every time you change *.mo files

mops test --watch

GitHub workflow to run tests

Now you can add GitHub workflow to run mops test on push and pull request.

  1. Run
mops template
  1. Select GitHub workflow

  2. Push changes to GitHub

3 Likes

Do I understand it correctly that all dependencies required for tests should be under [dev-dependencies] in the mops.toml on the top-level project directory? And there is no mops.toml file in the test directory, right?

Does the [dev-dependencies] have any other purpose besides for mops test?

mops test definitely requires dfx installed, or is it possible to do moc/wasmtime-only tests with it?

Yes

If you have published a package with dev dependencies, they will not be installed when someone installs your package.
If you are not going to publish a package, there is no difference between dev and non-dev deps.

You can set env var DFX_MOC_PATH=<path-to-moc> mops test to make mops use that moc instead of moc in dfx

May be useful: mocv - npm

1 Like

Interesting. So when that is set then I don’t even need dfx installed at all. But how is that possible? Without dfx it would need at least wasmtime to run the tests, or? It works for me with only moc and mops. I don’t explicitly install wasmtime nor dfx. How is that possible?

You can run motoko interpreter with moc -r <file>

1 Like

Update

Support for wasi mode

Some tests cannot be run with the moc interpreter. For example, when you use to_candid/from_candid, or if you get stackoverflow errors.

Now if you add // @testmode wasi as the first line to your test file, mops will compile it to wasm and execute it with wasmtime.

Faster test runs

mops test now runs tests in parallel which can increase speed by up to 2-3 times.

Tested on different projects:

mops 0.16 mops 0.17
1.0 sec 0.5 sec
3.4 sec 1.1 sec
28 sec 11 sec
50 sec 15 sec
6 Likes

This is great news! Well done to you for creating new testing tools. I tried to write tests using them and it makes work much easier. Thank you for your work! But do you ever plan to develop a coverage checker?

Not planned for the near future, but would definitely be a nice feature

I think mops is great. I’ve made two libraries with tests and documentation. Everything goes smoothly and looks great!

2 Likes

Update

Mops cli 0.18.0 now will show you failed line content.

before

after

4 Likes

Can you add an example of how to do this / add a command to return the moc path through mops?

To run test in wasi mode, you need to add // @testmode wasi as a first line, example:

mops has no such functionality, you can use

$(dfx cache show)/moc

or use mocv - npm

2 Likes

Update

Default test mode

Now you change the default test mode to wasi by adding the --mode wasi argument

New test reporters

More test reporters available:

  • verbose - prints each test name (default)
  • files - prints only test files
  • compact - pretty progress bar

How to use:

mops test --reporter compact

test-reporters

6 Likes

I used Mops and it’s test capability for the first time yesterday. It was a really great experience. Thank you for working on this @ZenVoich.

2 Likes

@LightningLad91 Thanks for the kind words :pray:

1 Like

Hi guys, I want to use custom users call my test canister. But I do not know how to set different accounts.
This example show I can create an agent:

import Debug "mo:base/Debug";

actor class MyCanister() {
	public query ({caller}) func getCaller() : async Principal {
		caller;
	};
};

actor class Agent(canister : MyCanister) {
	public func call() : async Principal {
		await canister.getCaller();
	};
};

let myCanister = await MyCanister();
let agent1 = await Agent(myCanister);
let agent2 = await Agent(myCanister);

Debug.print(debug_show(await agent1.call()));
Debug.print(debug_show(await agent2.call()));

But it seems the callers will be a canister not a user. At the same time I have to implement all the target functions in the agent. So any way to set the caller in mops test?

Time.now() seems to report 42. Is there a way to manipulate that? I need to test some things where I set created_at time to a day ago and my parameter is Nat64 so I can’t go negativie.