Persimmon用アサーションライブラリMuscleAssertを作った

正確には「作っていたライブラリをPersimmon.MuscleAssertにrenameした」です。

注意

この記事はあくまで私の考えでありpersimmon-projectsの総意であるわけではありません。

前提

そんなにF#寄りの話はしないはずなのでこれくらいの情報量で大丈夫なはず…?

Persimmonはpower assertの夢を見るか?

昨今はpower assertの認知度が格段に上がっているようですね。 これはJavaScript向けのpower-assertによるものが大きいのではないかと思われます。

ちょっと脱線しつつPersimmonにpower assertが必要か考えてみましょう。

どの意図でのpowert assert?

assertという言葉は二つの用途で使われいると思います。

  1. DbCとしての表明(assertion)
  2. checkingを目的としたassert関数

DbC向けの用途でpower assertが利用されることはまだ少ないようです。 たぶんGroovyとJavaScript*1くらいなのではという認識です。 私が知らないだけかもしれないのですが…。

一方、ユニットテストフレームワークの文脈上で登場するpower assertは様々な言語に存在します。 .NETにもhttps://github.com/PowerAssert/PowerAssert.Netが存在します。 使い勝手は触ったことがないのでよく知りません…。

今回は後者のほうでのpower assertについての言及です。

我々が欲しかったものはpower assertなのか

かつて、下記の記事が界隈をにぎわせました。

そろそろPower Assertについてひとこと言っておくか - ぐるぐる~

状況が少し変わっているのは

  • Groovy(こっちは当時から?)やJavaScriptのpower assertでは文字列の比較で差分を表示する

あたりでしょうか。 あとは下記も参照すると良いかもしれません。

https://github.com/persimmon-projects/Persimmon/issues/4#issuecomment-60347183

これにF#的な補足をしておくと

  • オブジェクトの一致を確認することが多い
    • 判別共用体やレコードなど型を作りやすい環境にあるため、文字列や数値などではなくプロパティを持ったオブジェクトを比較することになりやすい
  • パイプライン演算子でつなげるスタイルで結果をassert関数に渡す場合もある
    • Elixirのように強力なマクロがあるわけではないので途中の式が解析対象にできない
    • power assertのためにスタイルを変えるのか?的な

みたいな話があるようなないような感じです。

プロパティを幾つも持つオブジェクト(例えばJSONオブジェクトをマッピングしたレコード)をpower assertにかけると無駄に情報過多になりやすく、しかしinlineに式を書かないことのほうが多いため必要な情報が増えるかどうかは微妙なところです。 情報が増えないのにグラフィカルな失敗結果が表示されても嬉しいとは思えません。

はたしてこの状況で、苦労してpower assertを実装する必要があるのでしょうか?

少なくとも私には手を出す気力がおきませんでした*2

力技で不要な情報を握りつぶす

しかし、やはり単純なassert関数は力不足です。

// https://github.com/persimmon-projects/Persimmon/blob/cdba2509e5d48e4cdd47f292f3578b00371c85fc/src/Persimmon/Assertions.fs#L9
let assertEquals expected actual =
  if expected = actual then pass ()
  else fail (sprintf "Expect: %A\nActual: %A" expected actual)

上記はPersimmon 1.1.0のassertEqualsですが、とにかく貧弱です。 情報量がsprintfで出力可能な範囲に絞られるわけですが、こいつの表示はあまりあてにできないと思ったほうが良いです。 ひどいときには型名しか表示されません(それでも言語組み込みのassertよりはマシですが)。

これではさすがにつらい(らしい)のでもうちょっと情報量増やせないか、という話になるのは必然です。 しかしpower assertには懐疑的…どうしようという話になります。

さて、id:bleis-tiftさんの記事ではこう述べられていました。

ユニットテストにおいて、最も欲しいのは「どこがどうなっているか」ではなく、 「どこがどう違っているか」じゃないですかね。

これをPersimmon開発チームでもう少し協議した結果、「オブジェクトのdiffをだそう」という流れになりました。 というわけで出来上がった試作ライブラリがPersimmon.MuscleAssertです。

https://github.com/persimmon-projects/Persimmon.MuscleAssert

このライブラリができることは「等値比較し、不一致だったらオブジェクトの差分をひたすら表示する」ただ一つです。 とはいえ、実際には以下の制約がつきます。

  • System.Typeはアクセスした瞬間に例外が飛ぶプロパティもリフレクションで取れてしまうためFullNameプロパティのみ対象にする
  • IEnumerableはdiffの対象にしないで「無視したよ」と警告をだす
    • 無限リストのである可能性や、iterateする際に副作用が発生しオブジェクトが操作されてしまう可能性があるため
  • stringも未加工で表示
    • 今後の方針次第ではdiff-match-patchを導入するかもしれません

コード例と実行結果を見てみましょう。

open Persimmon
open UseTestNameByReflection
open Persimmon.MuscleAssert

type TestRecord = {
  X: string list
  Y: int
}

type TestDU =
  | A
  | B of TestRecord
  | C

let ``dump diff list includeing DU`` = parameterize {
  source [
    ([A; A], [A; C])
    ([A], [B { X = []; Y = 2 }])
    ([B { X = []; Y = 1 }], [B {X = []; Y = 2 }])
  ]
  run (fun (expected, actual) -> test {
    do! expected === actual
  })
}

テスト結果では以下のような表示が垣間見えるはずです。

Assertion violated ...
  1. .[1]
    left  TestDU.A
    right TestDU.C
Assertion violated ...
  1. .[0]
    left  TestDU.A
    right TestDU.B
Assertion violated ...
  1. .[0].Item.Y
    left  1
    right 2

値が異なる部分のみに表示を握り潰す、実に脳筋ですね。

命名理由

「普通のアサーションと区別しにくいから名前をつけよう」という話になった際に

これがなぜかそのまま採用されました。

power assertと対立する存在なのか?

不明です。

正直なところ、言語によってはpower assertのほうが使い勝手が良い可能性はもありますし、各種power assertライブラリがオブジェクトdiff機能を取り込めば話は変わってくるかもしれません。

とはいえ、一つだけ明確に言える違いがあります。 それはdiffを取得するだけであればASTの操作は必要ないという点です。 世の中にはテストでASTを操作することに懐疑的な派閥もあるらしいですが、そういった派閥にも受け入れられる可能性は残されています。

どんな言語でも実装できるのか

オブジェクト用diffライブラリはJavaから移植しました。 そのライブラリではgetプレフィックスのついたメソッドを対象としていたので、何かしらの制約(getterのような慣習的なもの)を設ければ実装できるのではないでしょうか。

おわりに

ユニットテストフレームワークには以下の方針や理想(?)があると思っています。

  • 成功時は沈黙し、失敗時はやかましく
  • 「なぜ失敗したのか」を的確に伝える
    • もしくは容易に推論可能な形で表示する

power assertがこのまま勢いを増すのか、筋肉式assertの流れが来るのか、第三の概念がかっさらうのかはわかりません。 ただ日々考え、実装し、議論を交わさなければ停滞するだけだと思います。

これを機会に一度、「テスト失敗時に欲しかった情報とは何か」を考えてみるのも良いかもしれません。

*1:https://github.com/power-assert-js にあるツール群を駆使すれば可能という話を聞きかじっただけなので、詳細はその方面の方に尋ねてください

*2:テストケースの構文自体を解析対象にしようと試みたことはありますが、これも情報過多になりそうだったので一旦開発を止めています https://github.com/persimmon-projects/Persimmon.Pudding