this repo has no description
at main 403 lines 9.5 kB view raw
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}