Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expect #4

Merged
merged 15 commits into from
Oct 26, 2023
30 changes: 30 additions & 0 deletions .github/workflows/mops-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: mops test

on:
push:
branches:
- main
- master
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18

- name: install moc
run: npx mocv use latest

- name: install mops
run: npm i ic-mops -g

- name: install mops packages
run: mops install

- name: run tests
run: mops test
228 changes: 206 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ Run `mops test` to run tests.
import {test} "mo:test";

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

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

Expand All @@ -36,13 +36,13 @@ Use `suite` to group your tests.
import {test; suite} "mo:test";

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

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

Expand All @@ -53,30 +53,214 @@ Use `skip` to skip tests.
import {test; skip} "mo:test";

skip("this test will never run", func() {
assert false;
assert false;
});

test("this test will run", func() {
assert true;
assert true;
});
```

## Async tests
If there are `await`'s in your tests, use functions from `mo:test/async`.
If there are `await`s in your tests, use functions from `mo:test/async`.

```motoko
import {test; suite} "mo:test/async";

await suite("my async test suite", func(): async () {
await test("async test", func(): async () {
let res = await myAsyncFn();
assert Result.isOk(res);
});

test("should generate unique values", func(): async () {
let a = await generate();
let b = await generate();
assert a != b;
});
await suite("my async test suite", func() : async () {
await test("async test", func() : async () {
let res = await myAsyncFn();
assert Result.isOk(res);
});

test("should generate unique values", func() : async () {
let a = await generate();
let b = await generate();
assert a != b;
});
});
```

# Expect

```motoko
import {test; expect} "mo:test";
```

Expect consists of a number of "matchers" that let you validate different things.

Compared to `assert`, in case of fail `expect` shows you the details of the values.

For example `assert`:
```motoko
assert myNat == 1;
// execution error, assertion failure
```
We only know that `myNat` is not equal to `1`, but what is the actual value of `myNat`?
To know this, we have to add a new line `Debug.print(debug_show(myNat))`.

but with `expect`:
```motoko
expect.nat(myNat).equal(1);
// execution error, explicit trap:
// ! received 22
// ! expected 1
```
here we see the actual value of `myNat`


## `expect.nat` (nat8, nat16, nat32, nat64, int, int8, int16, int32, int64)

```motoko
import {test; expect} "mo:test";

expect.nat(x).equal(10); // x == 10
expect.nat(x).notEqual(10); // x != 10
expect.nat(x).less(10); // x < 10
expect.nat(x).lessOrEqual(10); // x <= 10
expect.nat(x).greater(10); // x > 10
expect.nat(x).greaterOrEqual(10); // x >= 10

expect.int(x).equal(10); // x == 10 (Int)
expect.int64(x).equal(10); // x == 10 (Int64)
expect.nat32(x).equal(10); // x == 10 (Nat32)
```

## `expect.char`

```motoko
expect.char(c).equal('a'); // c == 'a'
expect.char(c).notEqual('a'); // c != 'a'
expect.char(c).less('a'); // c < 'a'
expect.char(c).lessOrEqual('a'); // c <= 'a'
expect.char(c).greater('a'); // c > 'a'
expect.char(c).greaterOrEqual('a'); // c >= 'a'
```

## `expect.text`

```motoko
expect.text(foo).equal("bar"); // foo == "bar"
expect.text(foo).notEqual("bar"); // foo != "bar"
expect.text(foo).contains("bar"); // Text.contains(foo, #text("bar"))
expect.text(foo).startsWith("bar"); // Text.startsWith(foo, #text("bar"))
expect.text(foo).endsWith("bar"); // Text.endsWith(foo, #text("bar"))"

expect.text(foo).less("bar"); // foo < "bar"
expect.text(foo).lessOrEqual("bar"); // foo <= "bar"
expect.text(foo).greater("bar"); // foo > "bar"
expect.text(foo).greaterOrEqual("bar"); // foo >= "bar"
```

## `expect.option`

```motoko
// optional Nat
let optNat = ?10;
expect.option(optNat, Nat.toText, Nat.equal).equal(?10); // optNat == ?10
expect.option(optNat, Nat.toText, Nat.equal).notEqual(?25); // optNat != ?25
expect.option(optNat, Nat.toText, Nat.equal).isNull(); // optNat == null


// optional custom type
type MyType = {
x : Nat;
y : Nat;
};

func showMyType(a : MyType) : Text {
debug_show(a);
};

func equalMyType(a : MyType, b : MyType) : Bool {
a.x == b.x and a.y == b.y
};

let val = ?{x = 1; y = 2};

expect.option(v, showMyType, equalMyType).notEqual(null);
expect.option(v, showMyType, equalMyType).isSome(); // != null
expect.option(v, showMyType, equalMyType).equal(?{x = 1; y = 2});
```

## `expect.result`

```motoko
type MyRes = Result.Result<Nat, Text>;

func show(a) = debug_show(a);
func equal(a, b) = a == b

let ok : MyRes = #ok(22);
let err : MyRes = #err("error");

expect.result<Nat, Text>(ok, show, equal).isOk();
expect.result<Nat, Text>(ok, show, equal).equal(#ok(22));

expect.result<Nat, Text>(err, show, equal).isErr();
expect.result<Nat, Text>(err, show, equal).equal(#err("error"));
expect.result<Nat, Text>(err, show, equal).equal(#err("other error"));
```

## `expect.principal`

```motoko
expect.principal(id).isAnonymous(); // Principal.isAnonymous(id)
expect.principal(id).notAnonymous(); // not Principal.isAnonymous(id)

expect.principal(id).equal(id2); // id == id2
expect.principal(id).notEqual(id2); // id != id2

expect.principal(id).less(id2); // id < id2
expect.principal(id).lessOrEqual(id2); // id <= id2
expect.principal(id).greater(id2); // id > id2
expect.principal(id).greaterOrEqual(id2); // id >= id2
```

## `expect.bool`
```motoko
expect.bool(x).isTrue(); // a == true
expect.bool(x).isFalse(); // a == false
expect.bool(x).equal(b); // a == b
expect.bool(x).notEqual(b); // a != b
```

## `expect.array`
```motoko
expect.array([1,2,3], Nat.toText, Nat.equal).equal([1,2,3]);
expect.array([1,2,3], Nat.toText, Nat.equal).notEqual([1,2]);

expect.array([1,2,3], Nat.toText, Nat.equal).contains(3); // array contains element 3
expect.array([1,2,3], Nat.toText, Nat.equal).notContains(10); // array does not contain element 10

expect.array([1,2,3,4], Nat.toText, Nat.equal).size(4);
```

## `expect.blob`
```motoko
expect.blob(blob).size(4); // blob.size() == 4
expect.blob(blob).equal(blob2); // blob == blob2
expect.blob(blob).notEqual(blob2); // blob != blob2

expect.blob(blob).less(blob2); // blob < blob2
expect.blob(blob).lessOrEqual(blob2); // blob <= blob2
expect.blob(blob).greater(blob2); // blob > blob2
expect.blob(blob).greaterOrEqual(blob2); // blob >= blob2
```

## `expect.call`

_Does not catch traps._

```motoko
func myFunc() : async () {
throw Error.reject("error");
};

func noop() : async () {
// do not throw an error
};

await expect.call(myFunc).reject(); // ok
await expect.call(noop).reject(); // fail
```
4 changes: 2 additions & 2 deletions mops.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name = "test"
version = "1.0.1"
description = "Motoko testing library to run tests with mops"
repository = "https://github.com/ZenVoich/test"
keywords = [ "test", "mops", "testing", "unit", "suite" ]
keywords = [ "test", "testing", "unit", "suite", "expect", "matchers", "mops" ]
license = "MIT"

[dependencies]
base = "0.8.1"
base = "0.10.0"
10 changes: 7 additions & 3 deletions src/async.mo
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import Debug "mo:base/Debug";
import {expect = _expect; fail = _fail} "./expect";

module {
public func test(name: Text, fn: () -> async ()): async () {
public func test(name : Text, fn : () -> async ()) : async () {
Debug.print("mops:1:start " # name);
await fn();
Debug.print("mops:1:end " # name);
};

public func suite(name: Text, fn: () -> async ()): async () {
public func suite(name : Text, fn : () -> async ()) : async () {
await test(name, fn);
};

public func skip(name: Text, fn: () -> async ()): async () {
public func skip(name : Text, fn : () -> async ()) : async () {
Debug.print("mops:1:skip " # name);
};

public let expect = _expect;
public let fail = _fail;
};
63 changes: 63 additions & 0 deletions src/expect/expect-array.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Array "mo:base/Array";
import Option "mo:base/Option";
import Debug "mo:base/Debug";
import {fail} "./utils";

module {
public class ExpectArray<T>(arr : [T], itemToText : (T) -> Text, itemEqual : (T, T) -> Bool) {
func _arrayToText(arr : [T], limit : Nat) : Text {
var text = "[";
label l do {
for (i in arr.keys()) {
text #= itemToText(arr[i]);

if (i + 1 < arr.size()) {
if (text.size() > limit) {
text #= "...";
break l;
};
text #= ", ";
};
};
text #= "]";
};
return text;
};

public func equal(other : [T]) {
if (not Array.equal<T>(arr, other, itemEqual)) {
fail(_arrayToText(arr, 100), "", _arrayToText(other, 100));
};
};

public func notEqual(other : [T]) {
if (Array.equal<T>(arr, other, itemEqual)) {
fail(_arrayToText(arr, 100), "", _arrayToText(other, 100));
};
};

public func contains(a : T) {
let has = Array.find<T>(arr, func b = itemEqual(a, b));
if (Option.isNull(has)) {
fail(_arrayToText(arr, 100), "to contain element", itemToText(a));
};
};

public func notContains(a : T) {
let has = Array.find<T>(arr, func b = itemEqual(a, b));
if (Option.isSome(has)) {
fail(_arrayToText(arr, 100), "to not contain element", itemToText(a));
};
};

public func size(n : Nat) {
if (arr.size() != n) {
fail(
"array size " # debug_show(arr.size()) # " - " # _arrayToText(arr, 50),
"array size",
debug_show(n)
);
};
};
};
};
Loading