Как сделать тестовую разработку в ocaml?

Я думаю, что все в названии, но я ищу специально для:

  • Что такое "стандартная" инфраструктура unit test в Ocaml?
  • Как интегрировать выполнение тестов в сборку?
  • Как автоматически выполнять тесты при каждом изменении файла?

В качестве бонуса я был бы заинтересован в инструментах тестирования покрытия...

Ответ 1

Кажется, что пакет ounit пользуется большой популярностью, есть несколько других пакетов, таких как kaputt или broken. Я являюсь автором последняя.

Я думаю, вас интересует как часть TDD, где тесты могут быть автоматизированы, вот как я это делаю в своих проектах. Вы можете найти несколько примеров на GitHub, таких как Lemonade или Rashell, которые имеют тест набор найден в соответствующих папках testsuite.

Обычно я работаю в соответствии с соответствующим рабочим процессом:

  • Я начинаю работать одновременно на тестах и ​​интерфейсах (.mli), таким образом, я пишу минимальную программу и не только пишу тестовый пример для функций, которые я хочу реализовать, но и имею возможность экспериментировать с интерфейсами чтобы иметь удобный интерфейс.

Например, для интерфейса с командой find(1), найденной в Rashell_Posix, я начал с написания тестовых примеров:

open Broken
open Rashell_Broken
open Rashell_Posix
open Lwt.Infix

let spec base = [
  (true,  0o700, [ base; "a"]);
  (true,  0o750, [ base; "a"; "b"]);
  (false, 0o600, [ base; "a"; "b"; "x"]);
  (false, 0o640, [ base; "a"; "y" ]);
  (true,  0o700, [ base; "c"]);
  (false, 0o200, [ base; "c"; "z"]);
]

let find_fixture =
  let filename = ref "" in
  let cwd = Unix.getcwd () in
  let changeto base =
    filename := base;
    Unix.chdir base;
    Lwt.return base
  in
  let populate base =
    Toolbox.populate (spec base)
  in
  make_fixture
    (fun () ->
       Lwt_main.run
         (Rashell_Mktemp.mktemp ~directory:true ()
          >>= changeto
          >>= populate))
    (fun () ->
       Lwt_main.run
         (Unix.chdir cwd;
          rm ~force:true ~recursive:true [ !filename ]
          |> Lwt_stream.junk_while (fun _ -> true)))

let assert_find id ?expected_failure ?workdir predicate lst =
  assert_equal id ?expected_failure
    ~printer:(fun fft lst -> List.iter (fun x -> Format.fprintf fft " %S" x) lst)
    (fun () -> Lwt_main.run(
         find predicate [ "." ]
         |> Lwt_stream.to_list
         |> Lwt.map (List.filter ((<>) "."))
         |> Lwt.map (List.sort Pervasives.compare)))
    ()
    lst

Функции spec и find_fixture используются для создания иерархии файлов с указанными именами и разрешениями для реализации функции find. Затем функция assert_find подготавливает тестовый пример, сравнивая результаты вызова с find(1) с ожидаемыми результатами:

  let find_suite =
    make_suite ~fixture:find_fixture "find" "Test suite for find(1)"
    |& assert_find "regular" (Has_kind(S_REG)) [
      "./a/b/x";
      "./a/y";
      "./c/z";
    ]
    |& assert_find "directory" (Has_kind(S_DIR)) [
      "./a";
      "./a/b";
      "./c"
    ]
    |& assert_find "group_can_read" (Has_at_least_permission(0o040)) [
      "./a/b";
      "./a/y"
    ]
    |& assert_find "exact_permission" (Has_exact_permission(0o640)) [
      "./a/y";
    ]

Одновременно я писал в файле интерфейса:

(** The type of file types. *)
type file_kind = Unix.file_kind =
  | S_REG
  | S_DIR
  | S_CHR
  | S_BLK
  | S_LNK
  | S_FIFO
  | S_SOCK

(** File permissions. *)
type file_perm = Unix.file_perm

(** File status *)
type stats = Unix.stats = {
  st_dev: int;
  st_ino: int;
  st_kind: file_kind;
  st_perm: file_perm;
  st_nlink: int;
  st_uid: int;
  st_gid: int;
  st_rdev: int;
  st_size: int;
  st_atime: float;
  st_mtime: float;
  st_ctime: float;
}

type predicate =
  | Prune
  | Has_kind of file_kind
  | Has_suffix of string
  | Is_owned_by_user of int
  | Is_owned_by_group of int
  | Is_newer_than of string
  | Has_exact_permission of int
  | Has_at_least_permission of int
  | Name of string (* Globbing pattern on basename *)
  | And of predicate list
  | Or of predicate list
  | Not of predicate

val find :
  ?workdir:string ->
  ?env:string array ->
  ?follow:bool ->
  ?depthfirst:bool ->
  ?onefilesystem:bool ->
  predicate -> string list -> string Lwt_stream.t
(** [find predicate pathlst] wrapper of the
    {{:http://pubs.opengroup.org/onlinepubs/9699919799/utilities/find.html} find(1)}
    command. *)
  1. Как только я был доволен своими тестовыми примерами и интерфейсами, я мог бы попытаться их скомпилировать, даже без реализации. Это возможно с bsdowl, просто предоставив файл интерфейса вместо файла реализации в Makefile. Здесь компиляция, вероятно, обнаружила несколько ошибок типа в моих тестах, которые я мог бы исправить.

  2. Когда тест скомпилирован для интерфейса, я мог бы реализовать эту функцию, начиная с функции alibi:

    найдем _ =  failwith "Rashell_Posix.find: не реализовано"

С этой реализацией я смог скомпилировать мою библиотеку и свой тестовый пакет. Конечно, в этот момент тест просто терпит неудачу.

  1. В этот момент мне просто нужно было реализовать функцию Rashell_Posix.find и повторить тесты до тех пор, пока они не пройдут.

Вот как я делаю тестовую разработку в OCaml, когда я использую автоматические тесты. Некоторые люди видят взаимодействие с REPL как форму тестового развития, это техника, которую я также люблю использовать, ее довольно просто настроить и использовать. Единственный шаг настройки для использования этой последней формы тестовой разработки в Rashell состоял в том, чтобы написать файл .ocamlinit для верхнего уровня, загружая все необходимые библиотеки. Этот файл выглядит так:

#use "topfind";;
#require "broken";;
#require "lemonade";;
#require "lwt.unix";;
#require "atdgen";;
#directory "/Users/michael/Workshop/rashell/src";;
#directory "/Users/michael/obj/Workshop/rashell/src";;

Две директивы #directory соответствуют каталогам источников и объектов.

(Отказ от ответственности: если вы внимательно посмотрите на историю, вы обнаружите, что я взял некоторые свободы с хронологией, но есть и другие проекты, в которых я выполняю именно этот путь - я точно не помню, какие именно).