如何在ocaml中进行test-driven开发?

How to do test-driven development in ocaml?

我想一切都在标题中,但我正在寻找:

作为奖励,我会对测试覆盖工具感兴趣...

好像是包ounit enjoys quite a large popularity, there are several other packages like kaputt or broken – 我是后者的作者

我猜您对 TDD 的特定部分感兴趣,其中测试可以自动化,下面是我在自己的项目中的做法。您可以在 GitHub 上找到一些示例,例如 Lemonade or Rashell,它们在各自的 testsuite 文件夹中都有一个测试套件。

通常我按照以下工作流程工作:

  1. 我开始同时做测试和接口(.mli)文件,这样我写了一个最小的程序,不仅为我想实现的功能写了一个测试用例,而且还有机会尝试界面以确保我有一个易于使用的界面。

例如,对于在 Rashell_Posix I started by writing test cases 中找到的 find(1) 命令的接口:

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

specfind_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";
    ]

同时我在写on the interface file:

(** 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 by just giving an interface file instead of an implementation file in the Makefile 实现。 这里的编译可能在我的测试中发现了一些我可以修复的类型错误。

  2. 测试接口编译时,我可以实现函数,从一个alibi函数开始:

    让我们找到_ = 失败 "Rashell_Posix.find: Not implemented"

通过这个实现,我能够编译我的库和测试套件。当然,在这一点上,测试只是失败了。

  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 指令对应于源和对象的目录。

(免责声明:如果你仔细看历史,你会发现我在时间顺序上做了一些改动,但还有其他项目我也是这样进行的——我只是记不清具体是哪些了。)