diff --git a/.github/workflows/mops-test.yml b/.github/workflows/mops-test.yml new file mode 100644 index 0000000..92d6783 --- /dev/null +++ b/.github/workflows/mops-test.yml @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 85d4c2f..1218ed6 100644 --- a/README.md +++ b/README.md @@ -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; }); ``` @@ -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; + }); }); ``` @@ -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; + +func show(a) = debug_show(a); +func equal(a, b) = a == b + +let ok : MyRes = #ok(22); +let err : MyRes = #err("error"); + +expect.result(ok, show, equal).isOk(); +expect.result(ok, show, equal).equal(#ok(22)); + +expect.result(err, show, equal).isErr(); +expect.result(err, show, equal).equal(#err("error")); +expect.result(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 +``` \ No newline at end of file diff --git a/mops.toml b/mops.toml index 0978f62..0c93038 100644 --- a/mops.toml +++ b/mops.toml @@ -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" \ No newline at end of file +base = "0.10.0" \ No newline at end of file diff --git a/src/async.mo b/src/async.mo index b493bb4..7aa56b3 100644 --- a/src/async.mo +++ b/src/async.mo @@ -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; }; \ No newline at end of file diff --git a/src/expect/expect-array.mo b/src/expect/expect-array.mo new file mode 100644 index 0000000..efdf4ce --- /dev/null +++ b/src/expect/expect-array.mo @@ -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(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(arr, other, itemEqual)) { + fail(_arrayToText(arr, 100), "", _arrayToText(other, 100)); + }; + }; + + public func notEqual(other : [T]) { + if (Array.equal(arr, other, itemEqual)) { + fail(_arrayToText(arr, 100), "", _arrayToText(other, 100)); + }; + }; + + public func contains(a : T) { + let has = Array.find(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(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) + ); + }; + }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-blob.mo b/src/expect/expect-blob.mo new file mode 100644 index 0000000..8a1eaeb --- /dev/null +++ b/src/expect/expect-blob.mo @@ -0,0 +1,28 @@ +import Blob "mo:base/Blob"; +import Array "mo:base/Array"; +import Option "mo:base/Option"; +import Debug "mo:base/Debug"; +import {bindCompare; fail} "./utils"; + +module { + public class ExpectBlob(blob : Blob) { + func show(v : Blob) : Text = "blob \"" # debug_show(v) # "\""; + + public let equal = bindCompare(blob, Blob.equal, show, ""); + public let notEqual = bindCompare(blob, Blob.notEqual, show, "!="); + public let less = bindCompare(blob, Blob.less, show, "<"); + public let lessOrEqual = bindCompare(blob, Blob.lessOrEqual, show, "<="); + public let greater = bindCompare(blob, Blob.greater, show, ">"); + public let greaterOrEqual = bindCompare(blob, Blob.greaterOrEqual, show, ">="); + + public func size(n : Nat) { + if (blob.size() != n) { + fail( + "blob size " # debug_show(blob.size()), + "blob size", + debug_show(n) + ); + }; + }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-bool.mo b/src/expect/expect-bool.mo new file mode 100644 index 0000000..6a84faa --- /dev/null +++ b/src/expect/expect-bool.mo @@ -0,0 +1,27 @@ +import Bool "mo:base/Bool"; +import {fail} "./utils"; + +module { + public class ExpectBool(a : Bool) { + public func isTrue() { + if (a != true) { + fail(Bool.toText(a), "", Bool.toText(true)); + }; + }; + public func isFalse() { + if (a != false) { + fail(Bool.toText(a), "", Bool.toText(false)); + }; + }; + public func equal(b : Bool) { + if (a != b) { + fail(Bool.toText(a), "", Bool.toText(b)); + }; + }; + public func notEqual(b : Bool) { + if (a == b) { + fail(Bool.toText(a), "to be !=", Bool.toText(b)); + }; + }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-call.mo b/src/expect/expect-call.mo new file mode 100644 index 0000000..06de36d --- /dev/null +++ b/src/expect/expect-call.mo @@ -0,0 +1,34 @@ +import Debug "mo:base/Debug"; +import Error "mo:base/Error"; +import {bindCompare} "./utils"; + +module { + public class ExpectCall(fn : () -> async ()) { + func show(t : Text) : Text = "\"" # t # "\""; + + public func reject() : async () { + try { + await fn(); + } + catch (err) { + if (Error.code(err) == #canister_reject) { + return; + }; + }; + Debug.trap("expected to throw error"); + }; + + // unable to catch + // public func trap() : async () { + // try { + // await fn(); + // } + // catch (err) { + // if (Error.code(err) == #canister_error) { + // return; + // }; + // }; + // Debug.trap("expected to trap"); + // }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-char.mo b/src/expect/expect-char.mo new file mode 100644 index 0000000..e1d5de3 --- /dev/null +++ b/src/expect/expect-char.mo @@ -0,0 +1,15 @@ +import Char "mo:base/Char"; +import {bindCompare} "./utils"; + +module { + public class ExpectChar(val : Char) { + func show(c : Char) : Text = "'" # Char.toText(c) # "'"; + + public let equal = bindCompare(val, Char.equal, show, ""); + public let notEqual = bindCompare(val, Char.notEqual, show, "!="); + public let less = bindCompare(val, Char.less, show, "<"); + public let lessOrEqual = bindCompare(val, Char.lessOrEqual, show, "<="); + public let greater = bindCompare(val, Char.greater, show, ">"); + public let greaterOrEqual = bindCompare(val, Char.greaterOrEqual, show, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-int.mo b/src/expect/expect-int.mo new file mode 100644 index 0000000..fcadfd6 --- /dev/null +++ b/src/expect/expect-int.mo @@ -0,0 +1,13 @@ +import Int "mo:base/Int"; +import {bindCompare} "./utils"; + +module { + public class ExpectInt(val : Int) { + public let equal = bindCompare(val, Int.equal, Int.toText, ""); + public let notEqual = bindCompare(val, Int.notEqual, Int.toText, "!="); + public let less = bindCompare(val, Int.less, Int.toText, "<"); + public let lessOrEqual = bindCompare(val, Int.lessOrEqual, Int.toText, "<="); + public let greater = bindCompare(val, Int.greater, Int.toText, ">"); + public let greaterOrEqual = bindCompare(val, Int.greaterOrEqual, Int.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-int16.mo b/src/expect/expect-int16.mo new file mode 100644 index 0000000..07fe8e8 --- /dev/null +++ b/src/expect/expect-int16.mo @@ -0,0 +1,13 @@ +import Int16 "mo:base/Int16"; +import {bindCompare} "./utils"; + +module { + public class ExpectInt16(val : Int16) { + public let equal = bindCompare(val, Int16.equal, Int16.toText, ""); + public let notEqual = bindCompare(val, Int16.notEqual, Int16.toText, "!="); + public let less = bindCompare(val, Int16.less, Int16.toText, "<"); + public let lessOrEqual = bindCompare(val, Int16.lessOrEqual, Int16.toText, "<="); + public let greater = bindCompare(val, Int16.greater, Int16.toText, ">"); + public let greaterOrEqual = bindCompare(val, Int16.greaterOrEqual, Int16.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-int32.mo b/src/expect/expect-int32.mo new file mode 100644 index 0000000..689f94e --- /dev/null +++ b/src/expect/expect-int32.mo @@ -0,0 +1,13 @@ +import Int32 "mo:base/Int32"; +import {bindCompare} "./utils"; + +module { + public class ExpectInt32(val : Int32) { + public let equal = bindCompare(val, Int32.equal, Int32.toText, ""); + public let notEqual = bindCompare(val, Int32.notEqual, Int32.toText, "!="); + public let less = bindCompare(val, Int32.less, Int32.toText, "<"); + public let lessOrEqual = bindCompare(val, Int32.lessOrEqual, Int32.toText, "<="); + public let greater = bindCompare(val, Int32.greater, Int32.toText, ">"); + public let greaterOrEqual = bindCompare(val, Int32.greaterOrEqual, Int32.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-int64.mo b/src/expect/expect-int64.mo new file mode 100644 index 0000000..a6ed901 --- /dev/null +++ b/src/expect/expect-int64.mo @@ -0,0 +1,13 @@ +import Int64 "mo:base/Int64"; +import {bindCompare} "./utils"; + +module { + public class ExpectInt64(val : Int64) { + public let equal = bindCompare(val, Int64.equal, Int64.toText, ""); + public let notEqual = bindCompare(val, Int64.notEqual, Int64.toText, "!="); + public let less = bindCompare(val, Int64.less, Int64.toText, "<"); + public let lessOrEqual = bindCompare(val, Int64.lessOrEqual, Int64.toText, "<="); + public let greater = bindCompare(val, Int64.greater, Int64.toText, ">"); + public let greaterOrEqual = bindCompare(val, Int64.greaterOrEqual, Int64.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-int8.mo b/src/expect/expect-int8.mo new file mode 100644 index 0000000..3005136 --- /dev/null +++ b/src/expect/expect-int8.mo @@ -0,0 +1,13 @@ +import Int8 "mo:base/Int8"; +import {bindCompare} "./utils"; + +module { + public class ExpectInt8(val : Int8) { + public let equal = bindCompare(val, Int8.equal, Int8.toText, ""); + public let notEqual = bindCompare(val, Int8.notEqual, Int8.toText, "!="); + public let less = bindCompare(val, Int8.less, Int8.toText, "<"); + public let lessOrEqual = bindCompare(val, Int8.lessOrEqual, Int8.toText, "<="); + public let greater = bindCompare(val, Int8.greater, Int8.toText, ">"); + public let greaterOrEqual = bindCompare(val, Int8.greaterOrEqual, Int8.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-nat.mo b/src/expect/expect-nat.mo new file mode 100644 index 0000000..0dec231 --- /dev/null +++ b/src/expect/expect-nat.mo @@ -0,0 +1,13 @@ +import Nat "mo:base/Nat"; +import {bindCompare} "./utils"; + +module { + public class ExpectNat(val : Nat) { + public let equal = bindCompare(val, Nat.equal, Nat.toText, ""); + public let notEqual = bindCompare(val, Nat.notEqual, Nat.toText, "!="); + public let less = bindCompare(val, Nat.less, Nat.toText, "<"); + public let lessOrEqual = bindCompare(val, Nat.lessOrEqual, Nat.toText, "<="); + public let greater = bindCompare(val, Nat.greater, Nat.toText, ">"); + public let greaterOrEqual = bindCompare(val, Nat.greaterOrEqual, Nat.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-nat16.mo b/src/expect/expect-nat16.mo new file mode 100644 index 0000000..c9dee32 --- /dev/null +++ b/src/expect/expect-nat16.mo @@ -0,0 +1,13 @@ +import Nat16 "mo:base/Nat16"; +import {bindCompare} "./utils"; + +module { + public class ExpectNat16(val : Nat16) { + public let equal = bindCompare(val, Nat16.equal, Nat16.toText, ""); + public let notEqual = bindCompare(val, Nat16.notEqual, Nat16.toText, "!="); + public let less = bindCompare(val, Nat16.less, Nat16.toText, "<"); + public let lessOrEqual = bindCompare(val, Nat16.lessOrEqual, Nat16.toText, "<="); + public let greater = bindCompare(val, Nat16.greater, Nat16.toText, ">"); + public let greaterOrEqual = bindCompare(val, Nat16.greaterOrEqual, Nat16.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-nat32.mo b/src/expect/expect-nat32.mo new file mode 100644 index 0000000..0749b6f --- /dev/null +++ b/src/expect/expect-nat32.mo @@ -0,0 +1,13 @@ +import Nat32 "mo:base/Nat32"; +import {bindCompare} "./utils"; + +module { + public class ExpectNat32(val : Nat32) { + public let equal = bindCompare(val, Nat32.equal, Nat32.toText, ""); + public let notEqual = bindCompare(val, Nat32.notEqual, Nat32.toText, "!="); + public let less = bindCompare(val, Nat32.less, Nat32.toText, "<"); + public let lessOrEqual = bindCompare(val, Nat32.lessOrEqual, Nat32.toText, "<="); + public let greater = bindCompare(val, Nat32.greater, Nat32.toText, ">"); + public let greaterOrEqual = bindCompare(val, Nat32.greaterOrEqual, Nat32.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-nat64.mo b/src/expect/expect-nat64.mo new file mode 100644 index 0000000..c86a7e5 --- /dev/null +++ b/src/expect/expect-nat64.mo @@ -0,0 +1,13 @@ +import Nat64 "mo:base/Nat64"; +import {bindCompare} "./utils"; + +module { + public class ExpectNat64(val : Nat64) { + public let equal = bindCompare(val, Nat64.equal, Nat64.toText, ""); + public let notEqual = bindCompare(val, Nat64.notEqual, Nat64.toText, "!="); + public let less = bindCompare(val, Nat64.less, Nat64.toText, "<"); + public let lessOrEqual = bindCompare(val, Nat64.lessOrEqual, Nat64.toText, "<="); + public let greater = bindCompare(val, Nat64.greater, Nat64.toText, ">"); + public let greaterOrEqual = bindCompare(val, Nat64.greaterOrEqual, Nat64.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-nat8.mo b/src/expect/expect-nat8.mo new file mode 100644 index 0000000..0286142 --- /dev/null +++ b/src/expect/expect-nat8.mo @@ -0,0 +1,13 @@ +import Nat8 "mo:base/Nat8"; +import {bindCompare} "./utils"; + +module { + public class ExpectNat8(val : Nat8) { + public let equal = bindCompare(val, Nat8.equal, Nat8.toText, ""); + public let notEqual = bindCompare(val, Nat8.notEqual, Nat8.toText, "!="); + public let less = bindCompare(val, Nat8.less, Nat8.toText, "<"); + public let lessOrEqual = bindCompare(val, Nat8.lessOrEqual, Nat8.toText, "<="); + public let greater = bindCompare(val, Nat8.greater, Nat8.toText, ">"); + public let greaterOrEqual = bindCompare(val, Nat8.greaterOrEqual, Nat8.toText, ">="); + }; +}; \ No newline at end of file diff --git a/src/expect/expect-option.mo b/src/expect/expect-option.mo new file mode 100644 index 0000000..c80902d --- /dev/null +++ b/src/expect/expect-option.mo @@ -0,0 +1,61 @@ +import Option "mo:base/Option"; +import {fail} "./utils"; + +module { + public class ExpectOption(a : ?T, toText : (T) -> Text, equalFn : (T, T) -> Bool) { + func _show(opt : ?T) : Text { + switch (opt) { + case (?v) "?" # toText(v); + case (null) "null"; + }; + }; + + public func _equal(b : ?T) : Bool { + switch (a) { + case (?aVal) { + switch (b) { + case (?bVal) { + if (equalFn(aVal, bVal)) { + true; + } + else { + false; + }; + }; + case (null) { + false; + }; + }; + }; + case (null) { + if (Option.isNull(b)) { + true; + } + else { + false; + }; + }; + }; + }; + + public func equal(b : ?T) { + if (not _equal(b)) { + fail(_show(a), "", _show(b)); + }; + }; + + public func notEqual(b : ?T) { + if (_equal(b)) { + fail(_show(a), "!=", _show(b)); + }; + }; + + public func isNull() { + equal(null); + }; + + public func isSome() { + notEqual(null); + }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-principal.mo b/src/expect/expect-principal.mo new file mode 100644 index 0000000..721acf3 --- /dev/null +++ b/src/expect/expect-principal.mo @@ -0,0 +1,28 @@ +import Principal "mo:base/Principal"; +import Array "mo:base/Array"; +import Option "mo:base/Option"; +import Debug "mo:base/Debug"; +import {bindCompare; fail} "./utils"; + +module { + public class ExpectPrincipal(val : Principal) { + func show(v : Principal) : Text = "principal \"" # Principal.toText(v) # "\""; + + public let equal = bindCompare(val, Principal.equal, show, ""); + public let notEqual = bindCompare(val, Principal.notEqual, show, "!="); + public let less = bindCompare(val, Principal.less, show, "<"); + public let lessOrEqual = bindCompare(val, Principal.lessOrEqual, show, "<="); + public let greater = bindCompare(val, Principal.greater, show, ">"); + public let greaterOrEqual = bindCompare(val, Principal.greaterOrEqual, show, ">="); + public func isAnonymous() { + if (not Principal.isAnonymous(val)) { + fail(show(val), "", show(Principal.fromBlob("\04"))); + }; + }; + public func notAnonymous() { + if (Principal.isAnonymous(val)) { + fail(show(val), "!=", show(Principal.fromBlob("\04"))); + }; + }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-result.mo b/src/expect/expect-result.mo new file mode 100644 index 0000000..5478167 --- /dev/null +++ b/src/expect/expect-result.mo @@ -0,0 +1,30 @@ +import Result "mo:base/Result"; +import {fail} "./utils"; + +module { + public class ExpectResult(val : Result.Result, toText : (Result.Result) -> Text, equalFn : (Result.Result, Result.Result) -> Bool) { + public func isOk() { + if (Result.isErr(val)) { + fail(toText(val), "", "#ok(...)"); + }; + }; + + public func isErr() { + if (Result.isOk(val)) { + fail(toText(val), "", "#err(...)"); + }; + }; + + public func equal(other : Result.Result) { + if (not equalFn(val, other)) { + fail(toText(val), "", toText(other)); + }; + }; + + public func notEqual(other : Result.Result) { + if (equalFn(val, other)) { + fail(toText(val), "!=", toText(other)); + }; + }; + }; +}; \ No newline at end of file diff --git a/src/expect/expect-text.mo b/src/expect/expect-text.mo new file mode 100644 index 0000000..a4cccec --- /dev/null +++ b/src/expect/expect-text.mo @@ -0,0 +1,18 @@ +import Text "mo:base/Text"; +import {bindCompare} "./utils"; + +module { + public class ExpectText(val : Text) { + func show(t : Text) : Text = "\"" # t # "\""; + + public let equal = bindCompare(val, Text.equal, show, ""); + public let notEqual = bindCompare(val, Text.notEqual, show, "!="); + public let less = bindCompare(val, Text.less, show, "<"); + public let lessOrEqual = bindCompare(val, Text.lessOrEqual, show, "<="); + public let greater = bindCompare(val, Text.greater, show, ">"); + public let greaterOrEqual = bindCompare(val, Text.greaterOrEqual, show, ">="); + public let contains = bindCompare(val, func(a : Text, b) = Text.contains(a, #text b), show, "to contain"); + public let startsWith = bindCompare(val, func(a : Text, b) = Text.startsWith(a, #text b), show, "to start with"); + public let endsWith = bindCompare(val, func(a : Text, b) = Text.endsWith(a, #text b), show, "to end with"); + }; +}; \ No newline at end of file diff --git a/src/expect/lib.mo b/src/expect/lib.mo new file mode 100644 index 0000000..9471544 --- /dev/null +++ b/src/expect/lib.mo @@ -0,0 +1,67 @@ +import Debug "mo:base/Debug"; +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Nat16 "mo:base/Nat16"; +import Nat32 "mo:base/Nat32"; +import Nat64 "mo:base/Nat64"; +import Int "mo:base/Int"; +import Int8 "mo:base/Int8"; +import Int16 "mo:base/Int16"; +import Int32 "mo:base/Int32"; +import Int64 "mo:base/Int64"; +import Float "mo:base/Float"; +import Char "mo:base/Char"; +import Text "mo:base/Text"; +import TrieMap "mo:base/TrieMap"; +import Hash "mo:base/Hash"; +import Array "mo:base/Array"; +import Option "mo:base/Option"; +import Bool "mo:base/Bool"; +import Iter "mo:base/Iter"; + +import {bindCompare; fail = _fail} "./utils"; +import ExpectInt "./expect-int"; +import ExpectInt8 "./expect-int8"; +import ExpectInt16 "./expect-int16"; +import ExpectInt32 "./expect-int32"; +import ExpectInt64 "./expect-int64"; +import ExpectNat "./expect-nat"; +import ExpectNat8 "./expect-nat8"; +import ExpectNat16 "./expect-nat16"; +import ExpectNat32 "./expect-nat32"; +import ExpectNat64 "./expect-nat64"; +import ExpectChar "./expect-char"; +import ExpectText "./expect-text"; +import ExpectBool "./expect-bool"; +import ExpectArray "./expect-array"; +import ExpectBlob "./expect-blob"; +import ExpectPrincipal "./expect-principal"; +import ExpectOption "./expect-option"; +import ExpectResult "./expect-result"; +import ExpectCall "./expect-call"; + +module { + public let expect = { + bool = ExpectBool.ExpectBool; + int = ExpectInt.ExpectInt; + int8 = ExpectInt8.ExpectInt8; + int16 = ExpectInt16.ExpectInt16; + int32 = ExpectInt32.ExpectInt32; + int64 = ExpectInt64.ExpectInt64; + nat = ExpectNat.ExpectNat; + nat8 = ExpectNat8.ExpectNat8; + nat16 = ExpectNat16.ExpectNat16; + nat32 = ExpectNat32.ExpectNat32; + nat64 = ExpectNat64.ExpectNat64; + char = ExpectChar.ExpectChar; + text = ExpectText.ExpectText; + array = ExpectArray.ExpectArray; + blob = ExpectBlob.ExpectBlob; + principal = ExpectPrincipal.ExpectPrincipal; + option = ExpectOption.ExpectOption; + result = ExpectResult.ExpectResult; + call = ExpectCall.ExpectCall; + }; + + public let fail = _fail; +}; \ No newline at end of file diff --git a/src/expect/utils.mo b/src/expect/utils.mo new file mode 100644 index 0000000..7dadafa --- /dev/null +++ b/src/expect/utils.mo @@ -0,0 +1,28 @@ +import Debug "mo:base/Debug"; + +module { + public func fail(actual : Text, condition : Text, reference : Text) { + let prefix = "\1b[31m!\1b[0m"; + var msg = "\n" # prefix # " \1b[30mreceived\1b[0m \1b[31m" # actual # "\1b[0m "; + if (condition != "") { + msg #= "\n" # prefix # " \1b[30mexpected\1b[0m" # " \1b[30m" # condition # "\1b[0m"; + } + else { + msg #= "\n" # prefix # " \1b[30mexpected\1b[0m"; + }; + msg #= " \1b[32m" # reference # "\1b[0m"; + + Debug.trap(msg); + }; + + public func compare(a : T, b : T, comp : (T, T) -> Bool, toText : (T) -> Text, condition : Text) { + let res = comp(a, b); + if (not res) { + fail(toText(a), condition, toText(b)); + }; + }; + + public func bindCompare(a : T, comp : (T, T) -> Bool, toText : (T) -> Text, condition : Text) : (T) -> () { + return func(b : T) = compare(a, b, comp, toText, condition); + }; +}; \ No newline at end of file diff --git a/src/lib.mo b/src/lib.mo index f1ad28b..dc8b787 100644 --- a/src/lib.mo +++ b/src/lib.mo @@ -1,17 +1,21 @@ import Debug "mo:base/Debug"; +import {expect = _expect; fail = _fail} "./expect"; module { - public func test(name: Text, fn: () -> ()) { + public func test(name : Text, fn : () -> ()) { Debug.print("mops:1:start " # name); fn(); Debug.print("mops:1:end " # name); }; - public func suite(name: Text, fn: () -> ()) { + public func suite(name : Text, fn : () -> ()) { test(name, fn); }; - public func skip(name: Text, fn: () -> ()) { + public func skip(name : Text, fn : () -> ()) { Debug.print("mops:1:skip " # name); }; + + public let expect = _expect; + public let fail = _fail; }; \ No newline at end of file diff --git a/test/expect.test.mo b/test/expect.test.mo new file mode 100644 index 0000000..c3af535 --- /dev/null +++ b/test/expect.test.mo @@ -0,0 +1,179 @@ +import Debug "mo:base/Debug"; +import Nat "mo:base/Nat"; +import Blob "mo:base/Blob"; +import Principal "mo:base/Principal"; +import Result "mo:base/Result"; +import Error "mo:base/Error"; +import {test; suite; expect; fail} "../src"; + +test("bool", func() { + expect.bool(true).isTrue(); + expect.bool(false).isFalse(); + expect.bool(true).equal(true); + expect.bool(false).equal(false); + expect.bool(true).notEqual(false); +}); + +test("option", func() { + expect.option(null, Nat.toText, Nat.equal).isNull(); + expect.option(?1, Nat.toText, Nat.equal).isSome(); + expect.option(?2, Nat.toText, Nat.equal).equal(?2); + expect.option(?3, Nat.toText, Nat.equal).notEqual(?44); + expect.option(?3, Nat.toText, Nat.equal).notEqual(null); + expect.option(null, Nat.toText, Nat.equal).equal(null); +}); + +test("option custom type", func() { + type MyType = { + x : Nat; + y : Nat; + }; + let v = ?{x = 1; y = 2}; + + func showMyType(a : MyType) : Text { + debug_show(a); + }; + + func equalMyType(a : MyType, b : MyType) : Bool { + a.x == b.x and a.y == b.y + }; + + expect.option(v, showMyType, equalMyType).notEqual(null); + expect.option(v, showMyType, equalMyType).equal(?{x = 1; y = 2}); +}); + +test("char", func() { + expect.char('a').equal('a'); + expect.char('a').notEqual('A'); +}); + +test("text", func() { + expect.text("hello motoko").endsWith("motoko"); + expect.text("hello motoko").contains("mot"); +}); + +test("nat", func() { + let myNat = 33; + expect.nat(myNat).notEqual(22); + expect.nat(myNat).equal(33); + expect.nat(myNat).less(66); +}); + +test("intX, natX", func() { + let myNat : Nat = 22; + let myNat8 : Nat8 = 33; + let myInt : Int = -44; + let myInt8 : Int8 = -44; + let myFloat : Float = 1.313; + expect.int(myNat).equal(22); + expect.nat8(myNat8).equal(33); + expect.nat(myNat).equal(22); + expect.nat(myNat).less(66); + + expect.int(myNat).notEqual(221); + expect.int8(myInt8).equal(myInt8); + expect.int64(123123123123).notEqual(1231231231232); + expect.nat8(myNat8).notEqual(32); + expect.nat(myNat).notEqual(221); + expect.nat8(myNat8).lessOrEqual(33); +}); + +test("array contains", func() { + expect.array([1,2,3,4,5,6,7,8,9,0], Nat.toText, Nat.equal).contains(6); + let exAr = expect.array([1,2,3,4,5,6,7,8,9,0], Nat.toText, Nat.equal); + exAr.contains(6); + exAr.contains(1); + exAr.contains(0); + exAr.notContains(88); + exAr.notContains(21); + exAr.size(10); +}); + +test("array size", func() { + expect.array([1,2,3,4,5,6,7,8,9,0], Nat.toText, Nat.equal).size(10); +}); + +test("array equal", func() { + expect.array([1,2,3,4], Nat.toText, Nat.equal).equal([1,2,3,4]); + expect.array([1,2,3,4], Nat.toText, Nat.equal).notEqual([1,2,2,4]); + expect.array([1,2,3,4], Nat.toText, Nat.equal).notEqual([1,2,3,4,5]); + expect.array([1,2,3,4], Nat.toText, Nat.equal).notEqual([1,2,3]); +}); + +test("blob", func() { + expect.blob(Blob.fromArray([1,2,3,4])).equal(Blob.fromArray([1,2,3,4])); + expect.blob(Blob.fromArray([1,2,3,4])).notEqual(Blob.fromArray([2,2,3,4])); + expect.blob(Blob.fromArray([1,2,3,4])).size(4); +}); + +test("principal", func() { + expect.principal(Principal.fromBlob(Blob.fromArray([1,2,3,4]))).equal(Principal.fromBlob(Blob.fromArray([1,2,3,4]))); + expect.principal(Principal.fromBlob(Blob.fromArray([1,2,3,4]))).notEqual(Principal.fromBlob(Blob.fromArray([1,2,3,5]))); + expect.principal(Principal.fromBlob("\04")).isAnonymous(); + expect.principal(Principal.fromBlob(Blob.fromArray([4]))).isAnonymous(); +}); + +test("result", func() { + type MyRes = Result.Result; + let ok : MyRes = #ok(22); + let err : MyRes = #err("error"); + + let expectOk = expect.result(ok, func(a) = debug_show(a), func(a, b) = a == b); + let expectErr = expect.result(err, func(a) = debug_show(a), func(a, b) = a == b); + + expectOk.isOk(); + expectOk.equal(#ok(22)); + + expectErr.isErr(); + expectErr.equal(#err("error")); + expectErr.notEqual(#err("other error")); +}); + +test("result opt ok", func() { + type MyRes = Result.Result; + let ok : MyRes = #ok(?22); + let err : MyRes = #err("error"); + + let expectOk = expect.result(ok, func(a) = debug_show(a), func(a, b) = a == b); + let expectErr = expect.result(err, func(a) = debug_show(a), func(a, b) = a == b); + + expectOk.isOk(); + expectOk.equal(#ok(?22)); + + expectErr.isErr(); + expectErr.equal(#err("error")); +}); + +test("expect custom", func() { + type Custom = { + x : Nat; + y : Nat; + }; + + class expectCustom(a : Custom) { + func show(x : Custom) : Text = debug_show(x); + + public func equal(b : Custom) { + if (a != b) { + fail(show(a), "to be ==", show(b)); + }; + }; + + public func greater(b : Custom) { + let ok = a.x > b.x and a.y > b.y; + if (not ok) { + fail(show(a), ">=", show(b)); + }; + }; + }; + + expectCustom({x = 1; y = 3}).equal({x = 1; y = 3}); + expectCustom({x = 2; y = 4}).greater({x = 1; y = 3}); +}); + +// test throw error +func myFunc() : async () { + throw Error.reject("error"); +}; + +await expect.call(myFunc).reject(); \ No newline at end of file