dog用のautodocライブラリを作った
Rubyにはautodocというライブラリがあります。
これのdog用のライブラリを作りました。
pocketberserker/dog-autodoc · GitHub
dog-autodoc用のsbtプラグインは実装中です…最低限部分できたので一旦リリースしました。
pocketberserker/sbt-dog-autodoc · GitHub
モチベーション
Scalaにはplay-autodocというものが存在する。
でもこれ、Play Framework用なんですよね。ということで
- Playに依存しないものにしたい
- どうせだからクライアントは切り替えたい -> httpzを使おう
というのがモチベーションです。
苦労した点
- sbtから出力ディレクトリを設定しつつ
- テストを一回だけ実行し
- 実行後に出力する
この流れをどうやってみたすかかなり悩んだ。 というか、当初の想定より随分と変わっている(テストの実装ミスしてて当初想定していた残骸がゴミコミットとして残ってしまった…orz)。
最終的には、
- Autodoc用のTestFrameworkを登録しておく
- AutodocMarker型の値が結果となっているケースのみ特別な処理をしてからイベントを登録する
- Autodocは実行したテスト結果とテストケース名から文字列をジェネレートできる
Event#fullyQualifiedName
にクラス名を入れるので、あとは出力結果(文字列)をどうにかしてEventに持たせれば保存に必要な情報が揃う- ところで、出力が可能なのはテストに成功したときのみ
- テスト成功時には通常
Event#throwable.isDefined
はfalse
を返すはずである。なぜなら、テスト成功なのに例外が投げられている状態は矛盾しているからだ。 - 実際JUnit Reporterはこの組み合わせを無視している https://github.com/sbt/sbt/blob/v0.13.9/testing/src/main/scala/sbt/JUnitXmlTestsListener.scala#L84
- つまり、この使われないであろう
Event#throwable
に出力を押し込んでおくことで、ファイル書き出しをsbt内だけで完結させることができる
- https://github.com/sbt/sbt/blob/v0.13.9/testing/src/main/scala/sbt/TestReportListener.scala#L9 を使ってイベントを取得してテスト終了時に保存
という形に。ひどい実装だ…。
こういう形になった最大の要因は型パラメータが邪魔をしてリフレクションでの取得 or 型の変換が難しかったからですね。以下そのときのxuwei-kさんのアドバイス。
@pocketberserker Scalaのリフレクション、(少なくとも2.10だと)スレッドセーフじゃないけど、2.10サポート諦める(もしくは頑張ってロックとって頑張る)んですか?
(さらに、2.11でもまだ直ってないという噂もある)
— Kenji Yoshida (@xuwei_k) September 3, 2015
@pocketberserker TestCaseをtype aliasやめて
case class TestCase[A](value: Kleisli[TestResult, Endo[Param], A])(implicit A: ClassTag[A])
とか?
— Kenji Yoshida (@xuwei_k) September 3, 2015
こういうときはType erasureが辛く感じます…。 結局、autodocのためにdog自体をいじるのはなんとなくいやだったので採用しませんでした。
まとめ
dogの作りが従来のユニットテスティングフレームワークと異なるからこういうときつらい…でもがんばる。
今後は安定かとかconsole用のWriterを復活させるかどうかあたりですかね。 あ、あとrename…ひどい命名ばかりしているので良い名前母種中です。
dogというScala用テストフレームワーク(?)を作ってみた
https://github.com/pocketberserker/dog
このフレームワークは Persimmon の移植というか Persimmon に存在していた型をよりMonadとかそういう方面に倒してみようというものです。
テストという存在の型について考えてみる - pocketberserkerの爆走
上記に途中経過は書きましたがそれから数日と経たずにいくつか変更が入ってます。
特徴
type TestCase[A] = Kleisli[TestResult, Endo[Param], A]
type AssertionResult[A] = NotPassedCause \/ A
- scalapropsフレンドリー
- イミュータブル、ミュータブルを意識しましょうてきな
モジュールとかその他リポジトリについて
- dog-core
- 基本となる型と関数をまとめたもの
- TestCase単体でタイムアウト設定して実行できるように scalaz.concurrent に依存している
- 上記の通り TestCase は Kleisli なので合成できる
- AssertionResult を合成することでいわゆる Soft Assertion を実現している
- dog
- sbtのtest-interfaceに依存
- scalapropsの実装をfork(実際にはコピーしたけど)して改修
- scalapropsとは異なり非同期実行しても問題ないつくりになっているがさてはてどうしたものやら
- gen
- scalaprops-genに依存
- scalaprops.Gen を使って入力値だけいい感じに生成してもらう
- 入力と期待値の集合は作れるけど性質までかけそうにない場合とかに…ぶっちゃけパラメタライズテストのかわり
- 固定値で回すくらいなら数撃ってあてようの方針
- props
- scalaprops-coreに依存
- PropertyやPropertiesとTestCaseの変換を目的としている
- sbt-dog 9割方sbt-scalapropsのコピー。scalaprops様様である(MITライセンスで助かった…)
- sbt-scalapropsに比べてぴーきーな挙動になりやすい
- dog-examplesサンプル集。雰囲気がつかめるかも?
ここまでで実装期間6日です。 正直ScalazとScalapropsがなければここまで楽に実装はできなかったと思うので xuwei-kさん ++
作った理由を改めて
- テストの合成とか型とかについてちゃんと検証したかった(某交流会の資料作成絡み)
- PersimmonのVS拡張や某ライブラリの移植に難航しているので息抜きしたかった
- 深夜のテンション
名前はリポジトリ作るときに"ドッグフーディングしたい"というのと"Co猫"という謎ワードがふってきて悩んだ結果であり、特に猫に張り合っているわけではないです。
今後の計画
Property#forAll
などをラップして性質を満たす時にGen#sample
をテスト結果として束縛できるようにしたい- Scala.js対応
- とはいえこれはリフレクションを使って実装している現状うまくいかない
- なので、scalaprops共々forkして別リポジトリで実装するつもり
- やりたいことあればてきとーに
こういうものがほしい、とかあればissueに登録していtだけるとうれしいです。
ScalaMatsuriにこれで応募してみるという手もあるが、さてはて。
テストという存在の型について考えてみる
いわゆるユニットテストフレームワークに現れる各種型について考えてみる。
成果は考えない。
このエントリは考えがまとまっていない段階で書かれたものであるため、間違っている可能性があります。
成果物(の途中経過)
途中結果を素っ飛ばした作成中のものは下記に公開している。
https://github.com/pocketberserker/dog/tree/d0ad112e00c72500a2ac5df764b7109249e684de
ドッグフーディングしたいのでこの名前になっただけである*1。
この記事では Scala with Scalaz と 一部 FSharp(というか Persimmon )を使って考察する。
テストケースは Kleisli
テストケースは入力値、実行事前条件、期待値、実行事後条件から構成されるはずだ。 このうち入力値、実行事前条件、実行事後条件は型の内部に隠せるものなので、たぶんよくあるテストケースはこんな感じになる。
trait TestCase { def run(): TestResult = { ??? } }
しかし、テスト結果を合成したい場合にはこれは圧倒的に情報が足りないので、テスト結果に値を持てるようにしたほうがよさそうである。
trait TestCase[A] { def run(): TestResult[A] = { ??? } } // 型コンストラクタはおいおい trait TestResult[A]
さて、これでTestCase#run
は 引数なしで実行できTestResult[A]
を返す操作が行える存在になった。
ここでちょっと考えると、引数なしというのは Unit
を引数にとる、と考えられなくもない。
そうすると都合の良いことに、Kleisli
の構造に一致する。
import scalaz._ type TestCase[A] = Kleisli[TestResult, Unit, A] // ちょっと不恰好だが、以下のように実行できる // testcase.run(())
テスト結果はMonadにできなくもない
テスト結果は多くの場合、成功か失敗かで表されるはずだ。
case class Success[A](value: A) extends TestResult[A] case class Failure[A](reason: String) extends TestResult[A]
しかし、これは色々と大雑把な構造になっている。
- エラーが考慮されていない
- 失敗時の情報が蓄積できない
- "失敗"なのか"テストをスキップ(無視)した"のか判断がつかない
問題1を解決するためにエラーを持てるようにする。
case class Done[A](result: String \/ A) extends TestResult[A] case class Error[A](e: Throwable) extends TestResult[A]
テスト実行完了か、エラーが発生したことが確認できるようになった。 この時点では明らかにMonadである。
問題2について解決を試みる。
case class Done[A](results: ValidationNel[String, A]) extends TestResult[A] case class Error[A](e: Throwable) extends TestResult[A]
成功時に保持すべき値は最終結果ひとつで良いのに対し、失敗情報は多数保持すべきなのでValidationNel
がちょうどよさそうだ。
ただし、実装次第ではMonad則を満たさなくなりそうである。
問題3が残っている。
これはValidationNel
だけでは解決できそうにないので、別の型を導入する。
trait Cause case class Violated(reason: String) extends Cause case class Skipped(reason: String) extends Cause trait AssertionResult[A] case class Passed[A](value: A) extends AssertionResult[A] case class NotPassed[A](cause: Cause) extends AssertionResult[A] type AssertionNel[A] = NonEmptyList[AssertionResult[A]] case class Done[A](results: AssertionNel[A]) extends TestResult[A] case class Error[A](e: Throwable) extends TestResult[A]
Done#results
に入る値を特定のものに絞り(ありえない状態を除外する、といったほうが正しい)、それ以外の入力は受け付けないようにすればTestResult[A]
はMonad則を満たす。
しかしその実装はMonadと呼べるのかというのは、疑問が尽きない。
Persimmon の TestResult は Monad ではない
今回はエラーだった場合はそれ以降何もしないことにしたので、Monadになる可能性があった。 しかし、実行できるアサーションはすべて実行してしまえ、という方針にするとMonad則を満たせなくなる。
Persimmon はそういう方針なので、おそらくMonad則を満たさない。
AssertionNel ~> TestResult
は実装できるか
これに関しては考え方次第である。
- テストケース名や入力値をテスト結果に持たせたいなら、無理
- 変換時は空のデータを持たせ、Runnerで後から差し替えるならできそう
検証が足りていないのでもう少し考慮すべき項目があるかもしれない。
Persimmonに関していえば、変換できなくもない。 テスト名が空な場合にはリフレクションで変数(関数)名前を取得して入れ替える機能が備わっているためだ。
Assertion[A]
は必要か
以下のような型を定義できる。
type Assertion[A] = Kleisli[AssertionResult, Unit, A]
しかし必要性を感じない。
基本的にさっさと実行されてしまうため、機能過多に思える。
これが何かに使えるか考えるなら、AssertionResult[A]
の機能拡充を考えたほうがよさそうである。
TestResult[A] は Applicative であるべきか
現時点のTestResult[A]
は入力値を制御しているためMonad則を満たすが、Applicativeにとどめておくべきかどうかは悩ましい問題だ。
Scalaのfor式は F# のコンピュテーション式に比べて表現に不安を感じるし、テスト失敗情報を蓄積させるならMonadにすべきではないのはその通りだ。 しかし、ScalaだとApplicativeで合成するのはそれはそれで面倒くさい作業にしかならない気もしている。
書きやすいDSLを実装する、という点ではScalaではマクロを使うことも考慮すべきなのだろうが…さてはて。
まとまらない
- FreeやFreeApを使って何かできないか考えていたらいつのまにかこういうものに行き着いた、なぜ
- Operational monad などの利用を考えると、やはりHaskellのライブラリを移植したほうが早いのだろうか…
- とはいえ、Scalazの各種型で色々定義できそうということがわかっただけでも面白かったので良しとする
*1:猫の双対でco猫とかにしてみたかったけどどう見ても名前負け思想だったのでやめた
#TestingFrameworkMeeting というイベントをやります
"なぜshapelessにはGeneric[Boolean]がないの?"と尋ねてみた
Gitterで質問したのでそのメモ*1。 なお私の英語は壊滅的なので片言?なのは許してください。
Hello. Why does Generic[Boolean] instance not exist?
と投稿したところ、milessabinさん(shapeless作者)から
Because there's no obvious sum of product representation of Boolean. I guess you would want false.type :+: true.type :+: CNil?
と返答があった。なるほど。
@milessabin Thank you. I understand. I try to port from Haskell code(using GHC.Generic) to Scala code. GHC.Generics derive some instances of Generic a.
とりあえずこういうことしようとしたんだよーと言ったところ
@pocketberserker gotcha. The idiom here is to hand-write instances for primitives and use Generic for ADTs. I would be very interested to see a side-by-side comparison of something written with GHC generics and something written with shapeless. Would you be able to blog about it?
ほえーと思ってたらなんか後日記事かくことになった。
まぁ、最初から無理っていうのもなんだし、やるだけやってみよう…。
*1:という名のバックアップ
「Persimmonでテストを書く」での疑問点に回答してみる
Persimmon でテストを書く — a wandering wolf
上記記事で2点ほどあがっていたものがあるので。
Usingがない
検証さぼってただけです、すみません。pull requestだしたのでそのうち入るかもしれません。
enable use keyword by pocketberserker · Pull Request #85 · persimmon-projects/Persimmon · GitHub
ただ、use
で束縛した値をreturn
すると思わぬ落とし穴にはまると思うので、Disposableな値は返さないように気を付けたほうが良いと思います。
Zeroがない
Zero、わりと影響範囲が大きいので悩みどころですね。
単純にunitな式を実行したいなら
// hoge: unit -> unit return hoge ()
もしくは
do hoge () return ()
と書けばよいと思います。
面倒くさいなら
type TestBuilder with member __.Zero() = ...
と型拡張を書くとかですかね。