F# 製ユニットテスティングフレームワーク Persimmon について #FsAdvent

この記事は F# Advent Calendar 2014 の27日目の記事です。

内容は今年の10月末あたりから作っている Persimmon というフレームワークについてです。

諸注意

本記事に書くことはあくまでpocketberserker個人の見解です。 他の開発者がどう考えているのかは

ソースコードやドキュメントなど

Persimmon

persimmon-projects/Persimmon · GitHub

Getting Started があるので使い方はそっちを参考にしてください。

あと public に書き込める場所がほしかったので Gitter に部屋を作りました。

persimmon-projects/ja - Gitter

柿は何であって何でないのか

F# の、 F# による、 F# のためのテスティングフレームワークです。

コンピュテーション式を前提にしている時点で他の言語で使うことは想定していません。 そして、少なくとも私は極力複数言語をプロダクションコードに採用したくない(メンテナンス面倒)ので、ほぼ F# オンリーでの開発になるでしょう。*1 そういうわけで上記の見解です。

F# でユニットテストというと NUnit (とその Wrapper の FsUnit)が多数派です。 ビルドツールの FAKE にはこのほかに xUnit.NET、 MSpec のヘルパー関数が用意されています。 FsCheck も結構使われていますが、あれは性質が異なるので今回の話では対象外です。

さて、この3つのテスティングフレームワークには共通点があります。 それは C# で実装されている点と、既存テスティングフレームワークの手法を踏襲して実装されている点です。

前者はともかく後者については、

  • 期待値と実行結果の型が異なっても実行時エラーでしか気づけないことがある
  • 属性ベースのテスト識別

という問題があります。 属性に関しては単に面倒くさいのと、渡せる型に制約があって一部F# の型は使えない点ですかね。

特徴と類似品

特徴はドキュメントに書いてあります。 まぁ、それだけでおわらせるのもアレなのでちょっとだけ追加。

副作用

Persimmon では、変数、unit を引数にとる関数、static member なプロパティがテスト実行の対象になります。 変数束縛で済ませるか関数にするかは、そのテストに副作用が含まれるかどうかで判断するのが良いのではと思っています。 unit をみたら副作用があるかもしれないと思え、ということですね。

型安全

NUnit の Assertion は型安全ではなかったりしますが、Persimmon の Assertion はよほど変なことをしない限りは型安全です。

コンピュテーション式

「属性を使わないなら、型で表現すればいいじゃない」

とはいえ、ただ型で表現するだけだと色々と面倒なこともあるので、Persimmon ではコンピュテーション式で記述できるようになっているとか、いないとか。

現時点で、master ブランチに4つのコンピュテーション式が存在します。

  • テストの記述
  • parameterized test の記述
  • 例外発生を期待する
  • Asyncな値を実行し非同期例外を補足する

テスト、アサーションの合成

Persimmon のテストやアサーションは値を返します。 そして、 unit を返すことは他のテストやアサーションに影響を与えないことを宣言することになります。 逆に、テストやアサーションの返り値を使って別のテストやアサーションを行うということは、それらが別のテスト、アサーションの通過/違反に依存していることを明示します。

テストを合成できることで

  • 状態を変更するテストと状態を変更しないテストの分離
  • テストの重複除去

を意識できるのではないか、と考えています。 あとはテストを解析することでテストの依存グラフを出力できるなるはずなので、何かしら役にたたないかなと。 テストを合成できるようにして依存グラフ云々みたいな話は過去に id:kyon-mm さんとも話していた気がするけど、参照実装や資料があるのかはちょっとわからないです…。

Monad ではない(はず)

何らかの制約をかければあるいは…ですが、結合則を証明できる気がしない。

AssertJ

最近になって存在を知った AssertJ というライブラリがあります。 Persimmon 実装中に kyon-mm さんに存在を教えてもらったのでこのライブラリの影響を受けた、というわけではないのですが、やりたいことの一部が似ています。

"継続可能なアサーション" は AssertJ では "Soft Assert"と呼ばれているようです。 この名前を踏襲するかは未定ですが、少なくとも新しい概念ではないのは確かなので、そのうちドキュメントに反映したいところです。

AssertJ の SoftAssertion との違いは

  • AssertJは任意、Persimmonは強制
  • AssertJの実装はstateful、Persimmonの実装はstateless
  • AssertJのAssertionは void だが、PersimmonのAssertionは値を返すことができる

SoftAssertionsが stateful なのはSoftAssertionのAPIドキュメントに書いてあります。 また、 assertAll メソッドを呼び出さないとテストに通るだろうこともこのドキュメントに書いてあります。

これはこれでありだと思いますが、合成できるなら合成したほうが楽かもね、とも思います。

NUnit, xUnit.NET, MSTest

主な違いは

  • 型安全性
  • 属性ベースと型ベースの違い
  • 例外によるハンドリングか値を返すか

です。

.NETの枠組みで既存の代替物となることはないでしょう。 テストとプロダクションコードが別言語でも良いと思っている人は移行する可能性はありますが、C# Wrapper を提供しない時点で C#er の方々が使う確率は減ります。 F#書きづらい(or 嫌い)という人もいるでしょうし。 あくまで、F#でのテストの代替物の可能性があるといいね、というレベルです。

モデル

1.0.0 αリリース以前にモデルが一度変更されています。 どれくらいかわったかというと これくらい

実装前の検討段階の資料はプロジェクトのWikiにありますが、最新版は存在しません。 というわけで、最新版の日本語情報を作ってみました。 ある程度固まったら英語に書き直してから日本語に再翻訳して公式ドキュメントに反映したいと思います。

エラーが AssetionResult<'T>ではなくTestResult<'T>にあるのは、例外発生時は多くの場合テストの続行自体不可能である場合が多いためです。

Visual Studio Test Explorer 対応

1.0.0 をリリースしたら作業を再開する予定です。 リフレクション使って実装しないといけないらしいので既にやる気が減衰していますが、まぁないと不便だし一回作れば楽になるのはわかっているので…冬休みの宿題ですね。

問題点

現時点で問題かなと思っていることをさらっとかいてみます。

assertion が貧弱

現状の Assertion は等値比較と述語の真偽比較くらいしか関数がないので貧弱です。 とはいえ、assertHoge 関数を増やしても覚えるのが面倒ですし、言葉つなぎ系は面倒なのであまり実装したくないなという話もあって今の状況なのですが…。

ぱっと思いつく解決策の一つは Power Assert ですが、この issue を見てもらうとわかる通りPower Assert ではない別の Assert を模索しようという方針になっています。

実践的なサンプルが少ない

これはそのうちアプリケーション作ってみないとなー、とかそういう話ですね。 今の FizzBuzz とポーカーの例だけでは情報量が少ない…。

一部既存ライブラリとの相性が悪い

NUnit や xUnit.NET と連携することを想定しているライブラリは、その性質上 例外を用いた Assertion の通知を行うものが多いです。 そしてこの方法は、Persimmon との相性はあまりよくありません。

こればかりはどうしようもないので、「ないなら作るしかねぇ」的な方針にせざるを得ないですね。 ぶっちゃけ FsCheck がそんな感じで相性が良いとは言えないので、 Persimmon 用に作ることを画策しています。

開発体制

今のところ3名で開発が行われています。

  • Gab-km さん
    • 原案発案
    • 怒涛のドキュメントレビュー(超重要)
    • 某ライブラリの開発があるため、現状Persimmonのコードを変更することは稀?
  • bleis-tift さん
    • 改良案発案
    • 新モデルの構築者
    • 忙しくて触れないことがあるようだ
  • pocketberserker
    • その他
    • 上記2名が動けない時に動く役

今のところバランスは保てていますが、今後開発速度をあげたいなら人手がほしいところ。

プロジェクト名の由来

かいつまんで書くと

  • 某社内チャットでプロジェクト名を募集
  • 「秋だから柿か栗では?」
  • とりあえず柿と栗の英単語を調べよう
  • Persimmon、よさそう
  • 「秋にちなんだ名前ってことは秋中に動くようにするんだよね?」「えっ?」

なぜ FAKE を使わないのか

趣味の問題です。

おわりに

1月までには TestExplorer 拡張を含めてリリースしたいところ…。

*1:GUIやパフォーマンスを考える場合はその限りではないです