|
| 1 | +open OUnit2 |
| 2 | + |
| 3 | +(*****************************************************************************) |
| 4 | +(* *) |
| 5 | +(* Unit Testing *) |
| 6 | +(* *) |
| 7 | +(* There are 2 main ways of testing in OCaml: using inline tests with pre- *) |
| 8 | +(* processors which live in the same file as the code we want to test, *) |
| 9 | +(* leveraging the power of testing frameworks such as OUnit, Alcotest, and *) |
| 10 | +(* QCheck; combined with a build system like Dune. *) |
| 11 | +(* *) |
| 12 | +(* For this example, I'm using OUnit2 in the same executable as the code *) |
| 13 | +(* file that contains the code I want to test which should normally live *) |
| 14 | +(* in a separate file. It is the most endorsed testing framework in OCaml *) |
| 15 | +(* and is very similar other familiar frameworks: you can group tests, *) |
| 16 | +(* assert different things, describe tests, skip them, mock, and more! *) |
| 17 | +(* *) |
| 18 | +(*****************************************************************************) |
| 19 | + |
| 20 | +module StringMap = Map.Make (String) |
| 21 | + |
| 22 | +module Testable : sig |
| 23 | + (** [Testable] contains code that should be otherwise consumed in a separate |
| 24 | + test file and should only expose the functions that the client of the |
| 25 | + library we are writing is concerned with. We can achieve this level of |
| 26 | + encapsulation through signatures, thus {b sealing} the module. *) |
| 27 | + |
| 28 | + val add : int -> int -> int |
| 29 | + (** [add a b] is the sum of integers [a] and [b]. *) |
| 30 | + |
| 31 | + val create_programmer |
| 32 | + : string |
| 33 | + -> int |
| 34 | + -> string |
| 35 | + -> string list |
| 36 | + -> string StringMap.t |
| 37 | + (** [create_programmer name age birth_date programming_languages] creates an |
| 38 | + immutable map/dictionary with 4 string keys corresponding to a programmer |
| 39 | + with a name, age, birth date and favourite programming languages. |
| 40 | +
|
| 41 | + Due to type constraints, all values of the map once created are of type |
| 42 | + [string], so the integer and list values need to be converted to their |
| 43 | + respective original types [int] and [string list] (comma separated) in |
| 44 | + order to use them properly. *) |
| 45 | +end = struct |
| 46 | + (* Alternative to a [Map], the way we define custom domain types in OCaml |
| 47 | + is through [records] but since the extra challenge requires a dictionary, |
| 48 | + I'm using a [Map] instead which slightly restricts the type system. *) |
| 49 | + |
| 50 | + let add = ( + ) |
| 51 | + |
| 52 | + let create_programmer name age birth_date programming_languages = |
| 53 | + StringMap.( |
| 54 | + empty |
| 55 | + |> add "name" name |
| 56 | + |> add "age" (string_of_int age) |
| 57 | + |> add "birth_date" birth_date |
| 58 | + |> add "programming_languages" (String.concat "," programming_languages)) |
| 59 | + ;; |
| 60 | +end |
| 61 | + |
| 62 | +let exercise_tests = |
| 63 | + "Exercise #13 Test Suite" |
| 64 | + >::: [ ("addition of two positive integers" |
| 65 | + >:: fun _ -> assert_equal 15 (Testable.add 5 10)) |
| 66 | + ; ("addition of two negative integers" |
| 67 | + >:: fun _ -> assert_equal (-90) (Testable.add (-40) (-50))) |
| 68 | + ; ("addition of negative and positive integers" |
| 69 | + >:: fun _ -> |
| 70 | + (* This test fails, but without the [printer] named argument set, it |
| 71 | + would print [not equal]. To improve the output, the argument needs |
| 72 | + to be set to a function that knows how to transform the type of |
| 73 | + the two compared values to [string]; in this case, [string_of_int] |
| 74 | + allows the test framework to output the following report: |
| 75 | +
|
| 76 | + {[ |
| 77 | + Error: Exercise #13 Test Suite:2: |
| 78 | + addition of negative and positive integers. |
| 79 | +
|
| 80 | + <SOME EXTRA INFO I OMITTED> |
| 81 | +
|
| 82 | + expected: 1 but got: 0 |
| 83 | + --------------------------------------------------------------- |
| 84 | + Ran: 3 tests in: 0.10 seconds. |
| 85 | + FAIL: Cases: 3 Tried: 3 Errors: 0 Failures: 1 Skip: 0 Todo: 0. |
| 86 | + ]} |
| 87 | +
|
| 88 | + The real output contains extra information such as the file where |
| 89 | + the assertion failure happened, the line and column, along with a |
| 90 | + small stack trace of which function call produced the failure. *) |
| 91 | + assert_equal 0 (Testable.add 9 (-9)); |
| 92 | + assert_equal ~printer:string_of_int 1 (Testable.add (-3) 3)) |
| 93 | + ] |
| 94 | +;; |
| 95 | + |
| 96 | +let _ = run_test_tt_main exercise_tests |
| 97 | + |
| 98 | +(**************************************************************************) |
| 99 | +(* *) |
| 100 | +(* Dificultad Extra (Opcional) *) |
| 101 | +(* *) |
| 102 | +(* Crea un diccionario con las siguientes claves y valores: *) |
| 103 | +(* - [name]: Tu nombre *) |
| 104 | +(* - [age]: Tu edad *) |
| 105 | +(* - [birth_date]: Tu fecha de nacimiento *) |
| 106 | +(* - [programming_languages]: Listado de lenguajes de programación *) |
| 107 | +(* *) |
| 108 | +(* Crea dos test: *) |
| 109 | +(* - Un primero que determine que existen todos los campos. *) |
| 110 | +(* - Un segundo que determine que los datos introducidos son correctos. *) |
| 111 | +(* *) |
| 112 | +(**************************************************************************) |
| 113 | + |
| 114 | +module DateValidator = struct |
| 115 | + open Core |
| 116 | + |
| 117 | + let years_of_date date_str = |
| 118 | + let today = Date.today ~zone:Time_float.Zone.utc in |
| 119 | + let date = Core.Date.of_string date_str in |
| 120 | + Date.diff today date / 365 |
| 121 | + ;; |
| 122 | + |
| 123 | + let validate_date_str date_str = |
| 124 | + let year, month, day = |
| 125 | + match String.split ~on:'/' date_str with |
| 126 | + | year :: month :: day :: _ -> |
| 127 | + int_of_string year, int_of_string month, int_of_string day |
| 128 | + | _ -> failwith "Date parsing failed" |
| 129 | + in |
| 130 | + year > 0 && month >= 1 && month <= 12 && day >= 1 && day <= 31 |
| 131 | + ;; |
| 132 | +end |
| 133 | + |
| 134 | +let challenge_tests = |
| 135 | + let mock = |
| 136 | + Testable.create_programmer |
| 137 | + "Luis Lopez" |
| 138 | + 32 |
| 139 | + "1992/04/09" |
| 140 | + [ "Haskell" |
| 141 | + ; "Clojure" |
| 142 | + ; "Typescript" |
| 143 | + ; "OCaml" |
| 144 | + ; "Elixir" |
| 145 | + ; "Java" |
| 146 | + ; "Python" |
| 147 | + ] |
| 148 | + in |
| 149 | + let open Testable in |
| 150 | + "Optional Challenge #13 Test Suite" |
| 151 | + >::: [ ("a mock programmer map has all fields set" |
| 152 | + >:: fun _ -> |
| 153 | + assert_bool "[name] field absent" (StringMap.mem "name" mock); |
| 154 | + assert_bool "[age] field absent" (StringMap.mem "age" mock); |
| 155 | + assert_bool |
| 156 | + "[birth_date] field absent" |
| 157 | + (StringMap.mem "birth_date" mock); |
| 158 | + assert_bool |
| 159 | + "[programming_languages] exists" |
| 160 | + (StringMap.mem "programming_languages" mock)) |
| 161 | + ; ("a mock programmer map has is valid and has the correct data" |
| 162 | + >:: fun _ -> |
| 163 | + let name = StringMap.find "name" mock in |
| 164 | + let age = int_of_string @@ StringMap.find "age" mock in |
| 165 | + let birth_date = StringMap.find "birth_date" mock in |
| 166 | + let programming_languages = |
| 167 | + StringMap.find "programming_languages" mock |
| 168 | + |> String.split_on_char ',' |
| 169 | + in |
| 170 | + assert_bool "[name] field is empty" (name <> ""); |
| 171 | + assert_equal "Luis Lopez" name; |
| 172 | + assert_bool "[age] field is not greater than 0" (age > 0); |
| 173 | + assert_equal 32 age; |
| 174 | + assert_bool "[birth_date] is empty" (birth_date <> ""); |
| 175 | + assert_bool |
| 176 | + "[birth_date] is invalid" |
| 177 | + (DateValidator.validate_date_str birth_date); |
| 178 | + assert_bool |
| 179 | + "[birth_date] conflicts with [age]" |
| 180 | + (DateValidator.years_of_date birth_date = 32); |
| 181 | + assert_equal "1992/04/09" birth_date; |
| 182 | + assert_equal |
| 183 | + programming_languages |
| 184 | + [ "Haskell" |
| 185 | + ; "Clojure" |
| 186 | + ; "Typescript" |
| 187 | + ; "OCaml" |
| 188 | + ; "Elixir" |
| 189 | + ; "Java" |
| 190 | + ; "Python" |
| 191 | + ]) |
| 192 | + ] |
| 193 | +;; |
| 194 | + |
| 195 | +let _ = run_test_tt_main challenge_tests |
0 commit comments