Skip to content

Latest commit

 

History

History
153 lines (119 loc) · 7.69 KB

doctest.md

File metadata and controls

153 lines (119 loc) · 7.69 KB

この文章について

こうなったらいいなという個人メモです。既に存在する機能についての解説ではありません。

OCaml のテストについて

これは OCaml のテストフレームワークについての紹介ではない。そういうの期待している人には役に立ちません。

OCaml にはいろいろテストフレームワークがあって、例えば OUnit とかがそうなのだけど、 今更 OUnit を素で使っている人はあまりいない。なんでかっていうと何か変なテストを グループ化するコンビネータがあって…それが便利だとは思えない。面倒臭い。 テスト書くの面倒臭かったら誰もテスト書かない。死あるのみ。

ということでテストをどう気軽に書いていくかなんだけれども、この頃、複雑なテストを どう走らせるか(例えば QuickCheck みたいにテストのためのデータを自動生成するとか、 小さいテストを組合せたテストが失敗した時に、どのサブテストがどの入力でコケたのか 範囲を狭めるとか)、より、どう気軽にテストをコードに書いて回すのか、ということを 考えている。

で今特に考えているのが doctest みたいなコメントにテストを解ておくやつ。 Doctest というのは、

  • 自然言語による機能説明に加えて、ワンラインで済む位の簡単な関数実行例がコメントあるととても判り易い
  • そういう関数実行例は挙動境界に近い微妙な入力例があると理解の助けになる
  • そしてこのような境界例はテストすべきだ。
  • コメントに書かれたコード実行例を抽出してテストできると素晴しい

という物で、いろんな言語にあるんだけど、 OCaml で OCaml doctest と検索しても出て来ない。 qtest http://batteries.vhugot.com/qtest/ というものがあって、テストをコメントの中に書けるんだけれども、これはドキュメント抽出されないので doctest にはならない。ドキュメントにならないのにコメントに書くのはどうなのか、という問題がある。いやそもそも doctest 自体に大きな問題がある。コードを書いてもコメントじゃないですか!:

  • コメントなので普通は無視される。テスト失敗以前の文法や型エラーがあっても気付かない。ドキュメントから抽出して実行しようとして初めて文法や型エラーがわかる。これは変だ。テスト失敗以外の問題は普通のビルドの時に気付きたい
  • コメントなのでエディタのコードハイライトとか補完とかそういう楽しい恩恵を受けることができない。短くてもテストはコードなので便利に書きたい

とか、まあそんな感じ。

私が欲しいのは何かというと、

  • テストを書く。エディタで色ついていたりすると嬉しい
  • テストはコードと共にすぐコンパイルされ、問題が早期チェックできる
  • テストコードはドキュメントに取り込まれるので実行例として参考になる

でそんなものができるかというと OCaml だと出来る。 val take : int -> 'a list -> 'a list という関数の重要な実行例というと こんなのだ:

take 0    [1;2;3;4;5] = [];;
take 3    [1;2;3;4;5] = [1;2;3];;
take 5    [1;2;3;4;5] = [1;2;3;4;5];;
take 8    [1;2;3;4;5] = [1;2;3;4;5];;
take (-1) [1;2;3;4;5] = []

これをこのままコードに書いておくと普通に実行されてしまうので、これはテスト だよと宣言することにする:

[%% TEST.take    (* <= take という名前のテストだよ *)
take 0    [1;2;3;4;5] = [];;
take 3    [1;2;3;4;5] = [1;2;3];;
take 5    [1;2;3;4;5] = [1;2;3;4;5];;
take 8    [1;2;3;4;5] = [1;2;3;4;5];;
take (-1) [1;2;3;4;5] = []
]

[%% ...] は OCaml で新しく入った extension というものでこの中にコードを 書くことができるのだけれども、このコードをプリプロセッサで処理して 別の OCaml コードに変換することができる(というか変換しないとエラーになる)。 だから何かうまいことプリプロセッサを書いて、例えば次のコードに変換すればよろしい:

let test_take () = 
  assert (take 0    [1;2;3;4;5] = []);
  assert (take 3    [1;2;3;4;5] = [1;2;3]);
  assert (take 5    [1;2;3;4;5] = [1;2;3;4;5]);
  assert (take 8    [1;2;3;4;5] = [1;2;3;4;5]);
  assert (take (-1) [1;2;3;4;5] = [])

あとはテスト時にはこれをどこからか呼び出し実行して、どこかで失敗する場合は その例外を受け取って表示する、とかまあ後はなんとでもなる。

ここまでで良い所は、

  • [%% ..] の中身の OCaml の文法で決っている、のでエディタ支援がこれを知っていればちゃんと 色がつくし、補完とかもできるはず(そういう支援があるのかという問題はとりあえずある)
  • [%% ..] はコードにすぐ抽出変換されるので、モジュール全体をコンパイルした 時点でテストのコンパイルにも成功している

て所。もしテストがコメントの中だとそういうわけにはいかない。

悪い所は、

  • テストはモジュール内に展開されるのでモジュールのオブジェクトサイズが増える

くらいだけれどもそもそも doctest は小さいテストしか書かないはずなので没問題だ。

さてこいつをドキュメントの中に埋め込むには…やはり PPX プリプロセッサが使えると思われる:

val take : int -> 'a list -> 'a list
(** [take n xs] takes the first [n] elements from the list [xs] *)

[%% TEST
take 0    [1;2;3;4;5] = [];;
take 3    [1;2;3;4;5] = [1;2;3];;
take 5    [1;2;3;4;5] = [1;2;3;4;5];;
take 8    [1;2;3;4;5] = [1;2;3;4;5];;
take (-1) [1;2;3;4;5] = []
]

Extension はかなり色んな所に書ける。例えば上の様にシグナチャ内部の値の宣言 のところにも書ける。これとその前にある OCamlDoc の docstring を上手いことプリプロセッサ でくっつければ次のようなコメントが生成できるはずだ:

val take : int -> 'a list -> 'a list
(** [take n xs] takes the first [n] elements from the list [xs]

- take 0    [1;2;3;4;5] = []
- take 3    [1;2;3;4;5] = [1;2;3];;
- take 5    [1;2;3;4;5] = [1;2;3;4;5]
- take 8    [1;2;3;4;5] = [1;2;3;4;5]
- take (-1) [1;2;3;4;5] = []
*)

これを OCamlDoc で HTML なり PDF になり出力すればみんなハッピーになれる。

あれでもこれはシグナチャー内部だから、実行例はドキュメントにはなったけど 実際のテストコードにはなっていない。それも多分プリプロセッサでできる:

let take n xs =
  let rec take_ n st xs =
    if n <= 0 then st
    else match xs with
    | [] -> st
    | x::xs -> take_ (n-1) (x::st) xs
  in
  List.rev (take_ n [] xs)

let test_take () = 
  assert (take 0    [1;2;3;4;5] = []);
  assert (take 3    [1;2;3;4;5] = [1;2;3]);
  assert (take 5    [1;2;3;4;5] = [1;2;3;4;5]);
  assert (take 8    [1;2;3;4;5] = [1;2;3;4;5]);
  assert (take (-1) [1;2;3;4;5] = [])

シグナチャに [%%TEST ..] が書かれている場合には、そのシグナチャに 対応したストラクチャ(実装コードのこと)を探して、その最後にテストコードを 変換展開すればいい。 OCaml の様にシグナチャとストラクチャを別ファイルに できる場合はちょっと苦労すると思われるが、不可能ではないはずだ。