Skip to content

Commit

Permalink
Merge pull request #4 from ZenVoich/expect
Browse files Browse the repository at this point in the history
Expect
  • Loading branch information
ZenVoich authored Oct 26, 2023
2 parents 866ab1d + c2216f6 commit c98b602
Show file tree
Hide file tree
Showing 27 changed files with 960 additions and 30 deletions.
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

0 comments on commit c98b602

Please sign in to comment.