|
1 | 1 | /// Unit testing for canisters |
2 | 2 | /// |
3 | 3 | /// The `Tester` class in this module can be used to define unit tests for canisters. |
4 | | -import Buffer "mo:base/Buffer"; |
5 | | -import List "mo:base/List"; |
6 | | -import Nat "mo:base/Nat"; |
7 | | -import M "Matchers"; |
| 4 | +import List "mo:core/pure/List"; |
| 5 | +import L "mo:core/List"; |
| 6 | +import Nat "mo:core/Nat"; |
8 | 7 |
|
9 | 8 | module { |
10 | 9 |
|
11 | | - public type Protocol = { #start : Nat; #cont : [Text]; #done : [Text] }; |
12 | | - public type TestResult = { #success; #fail : Text }; |
| 10 | + public type Protocol = { #start : Nat; #cont : [Text]; #done : [Text] }; |
| 11 | + public type TestResult = { #success; #fail : Text }; |
13 | 12 |
|
14 | | - type Test = (Text, () -> async TestResult); |
| 13 | + type Test = (Text, () -> async TestResult); |
15 | 14 |
|
16 | | - /// Instantiate one of these on canister initialization. Then you can use it to |
17 | | - /// register your tests and it will take care of running them. |
18 | | - /// |
19 | | - /// Use `runAll` for simple setups and `run` once that stops working. |
20 | | - /// |
21 | | - /// When using `run` the `Tester` will execute your tests in the right batch |
22 | | - /// sizes. It will keep calling your `test` function over and over, so make sure |
23 | | - /// to not do any work outside the registered tests. |
24 | | - /// |
25 | | - /// ```motoko |
26 | | - /// import Canister "canister:YourCanisterNameHere"; |
27 | | - /// import C "mo:matchers/Canister"; |
28 | | - /// import M "mo:matchers/Matchers"; |
29 | | - /// import T "mo:matchers/Testable"; |
30 | | - /// |
31 | | - /// actor { |
32 | | - /// let it = C.Tester({ batchSize = 8 }); |
33 | | - /// public shared func test() : async Text { |
34 | | - /// |
35 | | - /// it.should("greet me", func () : async C.TestResult = async { |
36 | | - /// let greeting = await Canister.greet("Christoph"); |
37 | | - /// M.attempt(greeting, M.equals(T.text("Hello, Christoph!"))) |
38 | | - /// }); |
39 | | - /// |
40 | | - /// it.shouldFailTo("greet him-whose-name-shall-not-be-spoken", func () : async () = async { |
41 | | - /// let greeting = await Canister.greet("Voldemort"); |
42 | | - /// ignore greeting |
43 | | - /// }); |
44 | | - /// |
45 | | - /// await it.runAll() |
46 | | - /// // await it.run() |
47 | | - /// } |
48 | | - /// } |
49 | | - /// ``` |
50 | | - public class Tester(options : { batchSize : Nat }) { |
51 | | - var tests : List.List<Test> = List.nil(); |
52 | | - var running : Bool = false; |
| 15 | + /// Instantiate one of these on canister initialization. Then you can use it to |
| 16 | + /// register your tests and it will take care of running them. |
| 17 | + /// |
| 18 | + /// Use `runAll` for simple setups and `run` once that stops working. |
| 19 | + /// |
| 20 | + /// When using `run` the `Tester` will execute your tests in the right batch |
| 21 | + /// sizes. It will keep calling your `test` function over and over, so make sure |
| 22 | + /// to not do any work outside the registered tests. |
| 23 | + /// |
| 24 | + /// ```motoko |
| 25 | + /// import Canister "canister:YourCanisterNameHere"; |
| 26 | + /// import C "mo:matchers/Canister"; |
| 27 | + /// import M "mo:matchers/Matchers"; |
| 28 | + /// import T "mo:matchers/Testable"; |
| 29 | + /// |
| 30 | + /// actor { |
| 31 | + /// let it = C.Tester({ batchSize = 8 }); |
| 32 | + /// public shared func test() : async Text { |
| 33 | + /// |
| 34 | + /// it.should("greet me", func () : async C.TestResult = async { |
| 35 | + /// let greeting = await Canister.greet("Christoph"); |
| 36 | + /// M.attempt(greeting, M.equals(T.text("Hello, Christoph!"))) |
| 37 | + /// }); |
| 38 | + /// |
| 39 | + /// it.shouldFailTo("greet him-whose-name-shall-not-be-spoken", func () : async () = async { |
| 40 | + /// let greeting = await Canister.greet("Voldemort"); |
| 41 | + /// ignore greeting |
| 42 | + /// }); |
| 43 | + /// |
| 44 | + /// await it.runAll() |
| 45 | + /// // await it.run() |
| 46 | + /// } |
| 47 | + /// } |
| 48 | + /// ``` |
| 49 | + public class Tester(options : { batchSize : Nat }) { |
| 50 | + var tests : List.List<Test> = List.empty(); |
| 51 | + var running : Bool = false; |
53 | 52 |
|
54 | | - /// Registers a test. You can use `attempt` to use a `Matcher` to |
55 | | - /// produce a `TestResult`. |
56 | | - public func should(name : Text, test : () -> async TestResult) { |
57 | | - if (running) return; |
58 | | - tests := List.push((name, test), tests); |
59 | | - }; |
60 | | - |
61 | | - /// Registers a test that should throw an exception. |
62 | | - public func shouldFailTo(name : Text, test : () -> async ()) { |
63 | | - if (running) return; |
64 | | - tests := List.push( |
65 | | - ( |
66 | | - name, |
67 | | - func() : async TestResult = async { |
68 | | - try { |
69 | | - let testResult = await test(); |
70 | | - #fail("Should've failed, but didn't"); |
71 | | - } catch _ { |
72 | | - #success; |
73 | | - }; |
74 | | - }, |
75 | | - ), |
76 | | - tests, |
77 | | - ); |
78 | | - }; |
| 53 | + /// Registers a test. You can use `attempt` to use a `Matcher` to |
| 54 | + /// produce a `TestResult`. |
| 55 | + public func should(name : Text, test : () -> async TestResult) { |
| 56 | + if (running) return; |
| 57 | + tests := List.pushFront(tests, (name, test)); |
| 58 | + }; |
79 | 59 |
|
80 | | - /// Runs all your tests in one go and returns a summary Text. If calling |
81 | | - /// this runs out of gas, try using `run` with a configured `batchSize`. |
82 | | - public func runAll() : async Text { |
83 | | - running := true; |
84 | | - var allTests = List.reverse(tests); |
85 | | - var result = ""; |
86 | | - var failed = 0; |
87 | | - var testCount = List.size(allTests); |
88 | | - label l loop { |
89 | | - switch allTests { |
90 | | - case null break l; |
91 | | - case (?((name, test), tl)) { |
92 | | - allTests := tl; |
93 | | - try { |
94 | | - result #= switch (await test()) { |
95 | | - case (#success) { |
96 | | - "\"" # name # "\"" # " succeeded.\n"; |
97 | | - }; |
98 | | - case (#fail(msg)) { |
99 | | - failed += 1; |
100 | | - "\"" # name # "\"" # " failed: " # msg # "\n"; |
101 | | - }; |
102 | | - }; |
103 | | - } catch _ { |
104 | | - failed += 1; |
105 | | - result #= "\"" # name # "\"" # "failed with an unexpected trap." # "\n"; |
106 | | - }; |
107 | | - }; |
108 | | - }; |
| 60 | + /// Registers a test that should throw an exception. |
| 61 | + public func shouldFailTo(name : Text, test : () -> async ()) { |
| 62 | + if (running) return; |
| 63 | + tests := List.pushFront( |
| 64 | + tests, |
| 65 | + ( |
| 66 | + name, |
| 67 | + func() : async TestResult = async { |
| 68 | + try { |
| 69 | + let _testResult = await test(); |
| 70 | + #fail("Should've failed, but didn't"); |
| 71 | + } catch _ { |
| 72 | + #success; |
109 | 73 | }; |
| 74 | + }, |
| 75 | + ), |
| 76 | + ); |
| 77 | + }; |
110 | 78 |
|
111 | | - if (failed == 0) { |
112 | | - result #= "Success! "; |
113 | | - } else { |
114 | | - result #= "Failure! "; |
| 79 | + /// Runs all your tests in one go and returns a summary Text. If calling |
| 80 | + /// this runs out of gas, try using `run` with a configured `batchSize`. |
| 81 | + public func runAll() : async Text { |
| 82 | + running := true; |
| 83 | + var allTests = List.reverse(tests); |
| 84 | + var result = ""; |
| 85 | + var failed = 0; |
| 86 | + var testCount = List.size(allTests); |
| 87 | + label l loop { |
| 88 | + switch allTests { |
| 89 | + case null break l; |
| 90 | + case (?((name, test), tl)) { |
| 91 | + allTests := tl; |
| 92 | + try { |
| 93 | + result #= switch (await test()) { |
| 94 | + case (#success) { |
| 95 | + "\"" # name # "\"" # " succeeded.\n"; |
| 96 | + }; |
| 97 | + case (#fail(msg)) { |
| 98 | + failed += 1; |
| 99 | + "\"" # name # "\"" # " failed: " # msg # "\n"; |
| 100 | + }; |
| 101 | + }; |
| 102 | + } catch _ { |
| 103 | + failed += 1; |
| 104 | + result #= "\"" # name # "\"" # "failed with an unexpected trap." # "\n"; |
115 | 105 | }; |
116 | | - result # Nat.toText(testCount - failed) # "/" # Nat.toText(testCount) # " succeeded."; |
| 106 | + }; |
117 | 107 | }; |
| 108 | + }; |
118 | 109 |
|
119 | | - /// You must call this as the last thing in your unit test. |
120 | | - public func run() : async Protocol { |
121 | | - if (not running) { |
122 | | - running := true; |
123 | | - tests := List.reverse(tests); |
124 | | - return #start(List.size(tests)); |
125 | | - }; |
126 | | - let results : Buffer.Buffer<Text> = Buffer.Buffer(options.batchSize); |
127 | | - var capacity = options.batchSize; |
128 | | - while (capacity > 0) { |
129 | | - capacity -= 1; |
130 | | - switch tests { |
131 | | - case null { |
132 | | - return #done(Buffer.toArray(results)); |
133 | | - }; |
134 | | - case (?((name, test), tl)) { |
135 | | - tests := tl; |
136 | | - try { |
137 | | - let testResult = await test(); |
138 | | - results.add( |
139 | | - switch testResult { |
140 | | - case (#success) { |
141 | | - "\"" # name # "\"" # " succeeded.\n"; |
142 | | - }; |
143 | | - case (#fail(msg)) { |
144 | | - "\"" # name # "\"" # " failed: " # msg # "\n"; |
145 | | - }; |
146 | | - } |
147 | | - ); |
148 | | - } catch _ { |
149 | | - results.add("\"" # name # "\"" # "failed with an unexpected trap." # "\n"); |
150 | | - }; |
151 | | - }; |
152 | | - }; |
153 | | - }; |
154 | | - return if (List.isNil(tests)) { |
155 | | - #done(Buffer.toArray(results)); |
156 | | - } else { |
157 | | - #cont(Buffer.toArray(results)); |
| 110 | + if (failed == 0) { |
| 111 | + result #= "Success! "; |
| 112 | + } else { |
| 113 | + result #= "Failure! "; |
| 114 | + }; |
| 115 | + result # Nat.toText(testCount - failed) # "/" # Nat.toText(testCount) # " succeeded."; |
| 116 | + }; |
| 117 | + |
| 118 | + /// You must call this as the last thing in your unit test. |
| 119 | + public func run() : async Protocol { |
| 120 | + if (not running) { |
| 121 | + running := true; |
| 122 | + tests := List.reverse(tests); |
| 123 | + return #start(List.size(tests)); |
| 124 | + }; |
| 125 | + let results : L.List<Text> = L.empty(); |
| 126 | + var capacity = options.batchSize; |
| 127 | + while (capacity > 0) { |
| 128 | + capacity -= 1; |
| 129 | + switch tests { |
| 130 | + case null { |
| 131 | + return #done(L.toArray(results)); |
| 132 | + }; |
| 133 | + case (?((name, test), tl)) { |
| 134 | + tests := tl; |
| 135 | + try { |
| 136 | + let testResult = await test(); |
| 137 | + L.add(results, |
| 138 | + switch testResult { |
| 139 | + case (#success) { |
| 140 | + "\"" # name # "\"" # " succeeded.\n"; |
| 141 | + }; |
| 142 | + case (#fail(msg)) { |
| 143 | + "\"" # name # "\"" # " failed: " # msg # "\n"; |
| 144 | + }; |
| 145 | + } |
| 146 | + ); |
| 147 | + } catch _ { |
| 148 | + L.add(results, "\"" # name # "\"" # "failed with an unexpected trap." # "\n"); |
158 | 149 | }; |
| 150 | + }; |
159 | 151 | }; |
| 152 | + }; |
| 153 | + return if (List.isEmpty(tests)) { |
| 154 | + #done(L.toArray(results)); |
| 155 | + } else { |
| 156 | + #cont(L.toArray(results)); |
| 157 | + }; |
160 | 158 | }; |
| 159 | + }; |
161 | 160 | }; |
0 commit comments