this repo has no description
1{0 Building a Test Framework}
2
3@scrolly Building a Test Framework in OCaml
4{ol
5{li
6 {b A Single Assertion}
7
8 The simplest possible test: check that a condition holds.
9 If it fails, raise an exception with a message. This is
10 the foundation everything else builds on.
11
12 {[
13exception Test_failure of string
14
15let assert_equal ~expected ~actual msg =
16 if expected <> actual then
17 raise (Test_failure
18 (Printf.sprintf "%s: expected %s, got %s"
19 msg
20 (string_of_int expected)
21 (string_of_int actual)))
22 ]}
23}
24{li
25 {b Collecting Tests}
26
27 A test is a named function. We store tests in a mutable list
28 so they can be registered declaratively with a simple helper.
29 Each test is just a unit function that might raise.
30
31 {[
32exception Test_failure of string
33
34let assert_equal ~expected ~actual msg =
35 if expected <> actual then
36 raise (Test_failure
37 (Printf.sprintf "%s: expected %s, got %s"
38 msg
39 (string_of_int expected)
40 (string_of_int actual)))
41
42type test = {
43 name : string;
44 fn : unit -> unit;
45}
46
47let tests : test list ref = ref []
48
49let register name fn =
50 tests := { name; fn } :: !tests
51
52let () = register "addition" (fun () ->
53 assert_equal ~expected:4 ~actual:(2 + 2)
54 "two plus two")
55
56let () = register "multiplication" (fun () ->
57 assert_equal ~expected:6 ~actual:(2 * 3)
58 "two times three")
59 ]}
60}
61{li
62 {b A Test Runner}
63
64 The runner iterates through registered tests, catching
65 exceptions to report pass or fail. It counts results
66 and prints a summary at the end.
67
68 {[
69exception Test_failure of string
70
71let assert_equal ~expected ~actual msg =
72 if expected <> actual then
73 raise (Test_failure
74 (Printf.sprintf "%s: expected %s, got %s"
75 msg
76 (string_of_int expected)
77 (string_of_int actual)))
78
79type test = {
80 name : string;
81 fn : unit -> unit;
82}
83
84let tests : test list ref = ref []
85
86let register name fn =
87 tests := { name; fn } :: !tests
88
89type result =
90 | Pass
91 | Fail of string
92
93let run_one test =
94 try test.fn (); Pass
95 with
96 | Test_failure msg -> Fail msg
97 | exn -> Fail (Printexc.to_string exn)
98
99let run_all () =
100 let results =
101 List.rev !tests
102 |> List.map (fun t -> (t.name, run_one t))
103 in
104 let passed =
105 List.length
106 (List.filter
107 (fun (_, r) -> r = Pass) results)
108 in
109 let total = List.length results in
110 List.iter (fun (name, result) ->
111 match result with
112 | Pass ->
113 Printf.printf " PASS %s\n" name
114 | Fail msg ->
115 Printf.printf " FAIL %s: %s\n" name msg
116 ) results;
117 Printf.printf "\n%d/%d tests passed\n"
118 passed total;
119 if passed < total then exit 1
120 ]}
121}
122{li
123 {b Better Assertions}
124
125 Real frameworks need more than integer equality. We add
126 string comparison, boolean checks, and a generic raises
127 assertion that checks an exception is thrown.
128
129 {[
130exception Test_failure of string
131
132let assert_equal ~expected ~actual msg =
133 if expected <> actual then
134 raise (Test_failure
135 (Printf.sprintf "%s: expected %s, got %s"
136 msg
137 (string_of_int expected)
138 (string_of_int actual)))
139
140let assert_string_equal ~expected ~actual msg =
141 if expected <> actual then
142 raise (Test_failure
143 (Printf.sprintf
144 "%s: expected %S, got %S"
145 msg expected actual))
146
147let assert_true condition msg =
148 if not condition then
149 raise (Test_failure msg)
150
151let assert_raises fn msg =
152 try fn ();
153 raise (Test_failure
154 (msg ^ ": expected exception"))
155 with
156 | Test_failure _ as e -> raise e
157 | _ -> ()
158
159type test = {
160 name : string;
161 fn : unit -> unit;
162}
163
164let tests : test list ref = ref []
165
166let register name fn =
167 tests := { name; fn } :: !tests
168
169type result = Pass | Fail of string
170
171let run_one test =
172 try test.fn (); Pass
173 with
174 | Test_failure msg -> Fail msg
175 | exn -> Fail (Printexc.to_string exn)
176
177let run_all () =
178 let results =
179 List.rev !tests
180 |> List.map (fun t -> (t.name, run_one t))
181 in
182 let passed = List.length
183 (List.filter
184 (fun (_, r) -> r = Pass) results) in
185 let total = List.length results in
186 List.iter (fun (name, result) ->
187 match result with
188 | Pass ->
189 Printf.printf " PASS %s\n" name
190 | Fail msg ->
191 Printf.printf " FAIL %s: %s\n"
192 name msg
193 ) results;
194 Printf.printf "\n%d/%d tests passed\n"
195 passed total;
196 if passed < total then exit 1
197 ]}
198}
199{li
200 {b Test Suites}
201
202 As projects grow, tests need organization. We add a suite
203 concept that groups related tests under a name. Suites
204 can be nested and run independently.
205
206 {[
207exception Test_failure of string
208
209let assert_equal ~expected ~actual msg =
210 if expected <> actual then
211 raise (Test_failure
212 (Printf.sprintf "%s: expected %s, got %s"
213 msg
214 (string_of_int expected)
215 (string_of_int actual)))
216
217let assert_string_equal ~expected ~actual msg =
218 if expected <> actual then
219 raise (Test_failure
220 (Printf.sprintf "%s: expected %S, got %S"
221 msg expected actual))
222
223let assert_true condition msg =
224 if not condition then
225 raise (Test_failure msg)
226
227let assert_raises fn msg =
228 try fn ();
229 raise (Test_failure
230 (msg ^ ": expected exception"))
231 with Test_failure _ as e -> raise e | _ -> ()
232
233type test = { name : string; fn : unit -> unit }
234type result = Pass | Fail of string
235
236type suite = {
237 suite_name : string;
238 mutable suite_tests : test list;
239}
240
241let suites : suite list ref = ref []
242
243let create_suite name =
244 let s = { suite_name = name;
245 suite_tests = [] } in
246 suites := s :: !suites; s
247
248let add_test suite name fn =
249 suite.suite_tests <-
250 { name; fn } :: suite.suite_tests
251
252let run_one test =
253 try test.fn (); Pass
254 with
255 | Test_failure msg -> Fail msg
256 | exn -> Fail (Printexc.to_string exn)
257
258let run_suite suite =
259 Printf.printf "Suite: %s\n" suite.suite_name;
260 let results =
261 List.rev suite.suite_tests
262 |> List.map (fun t ->
263 (t.name, run_one t)) in
264 let passed = List.length
265 (List.filter
266 (fun (_, r) -> r = Pass) results) in
267 let total = List.length results in
268 List.iter (fun (name, result) ->
269 match result with
270 | Pass ->
271 Printf.printf " PASS %s\n" name
272 | Fail msg ->
273 Printf.printf " FAIL %s: %s\n"
274 name msg
275 ) results;
276 Printf.printf " %d/%d passed\n\n"
277 passed total;
278 passed = total
279
280let run_all_suites () =
281 let all_ok = List.for_all run_suite
282 (List.rev !suites) in
283 if not all_ok then exit 1
284 ]}
285}
286{li
287 {b Expect Tests}
288
289 The crown jewel: expect tests capture actual output and
290 compare it to an expected snapshot. On first run, they
291 record the output. On later runs, they detect regressions.
292 This is how tools like ppx_expect and Cram tests work.
293
294 {[
295exception Test_failure of string
296
297let assert_equal ~expected ~actual msg =
298 if expected <> actual then
299 raise (Test_failure
300 (Printf.sprintf "%s: expected %s, got %s"
301 msg
302 (string_of_int expected)
303 (string_of_int actual)))
304
305let assert_string_equal ~expected ~actual msg =
306 if expected <> actual then
307 raise (Test_failure
308 (Printf.sprintf "%s: expected %S, got %S"
309 msg expected actual))
310
311let assert_true condition msg =
312 if not condition then
313 raise (Test_failure msg)
314
315let assert_raises fn msg =
316 try fn ();
317 raise (Test_failure
318 (msg ^ ": expected exception"))
319 with Test_failure _ as e -> raise e | _ -> ()
320
321type test = { name : string; fn : unit -> unit }
322type result = Pass | Fail of string
323
324type suite = {
325 suite_name : string;
326 mutable suite_tests : test list;
327}
328
329let suites : suite list ref = ref []
330
331let create_suite name =
332 let s = { suite_name = name;
333 suite_tests = [] } in
334 suites := s :: !suites; s
335
336let add_test suite name fn =
337 suite.suite_tests <-
338 { name; fn } :: suite.suite_tests
339
340let run_one test =
341 try test.fn (); Pass
342 with
343 | Test_failure msg -> Fail msg
344 | exn -> Fail (Printexc.to_string exn)
345
346(* Expect test infrastructure *)
347let expect_dir = "_expect"
348
349let expect_test suite name fn =
350 add_test suite name (fun () ->
351 let buf = Buffer.create 256 in
352 fn (Buffer.add_string buf);
353 let actual = Buffer.contents buf in
354 let path = Printf.sprintf "%s/%s/%s.expected"
355 expect_dir suite.suite_name name in
356 if Sys.file_exists path then begin
357 let ic = open_in path in
358 let expected = really_input_string ic
359 (in_channel_length ic) in
360 close_in ic;
361 assert_string_equal
362 ~expected ~actual
363 (name ^ " snapshot")
364 end else begin
365 let dir = Filename.dirname path in
366 ignore (Sys.command
367 ("mkdir -p " ^ dir));
368 let oc = open_out path in
369 output_string oc actual;
370 close_out oc;
371 Printf.printf
372 " NEW %s (snapshot saved)\n" name
373 end)
374
375let run_suite suite =
376 Printf.printf "Suite: %s\n" suite.suite_name;
377 let results =
378 List.rev suite.suite_tests
379 |> List.map (fun t ->
380 (t.name, run_one t)) in
381 let passed = List.length
382 (List.filter
383 (fun (_, r) -> r = Pass) results) in
384 let total = List.length results in
385 List.iter (fun (name, result) ->
386 match result with
387 | Pass ->
388 Printf.printf " PASS %s\n" name
389 | Fail msg ->
390 Printf.printf " FAIL %s: %s\n"
391 name msg
392 ) results;
393 Printf.printf " %d/%d passed\n\n"
394 passed total;
395 passed = total
396
397let run_all_suites () =
398 let all_ok = List.for_all run_suite
399 (List.rev !suites) in
400 if not all_ok then exit 1
401 ]}
402}
403}