続・そろそろPower Assertについてひとこと言っておくか

3年前にこんな記事をあげました。

bleis-tift.hatenablog.com

3行でまとめると、

  • Power Assertはユニットテストのためにほしかったものではない
  • 欲しいのは結果の差分
  • 誰か作って!

というエントリでした。 そしたら id:pocketberserker が作ってくれました!

github.com

PowerAssertより強そうな名前でいい感じです。

MuscleAssertの使い方

このライブラリは、PersimmonというF#用のテスティングフレームワークを拡張するライブラリとして作られています。 ただ、ざっくり概要をつかむだけであればどちらも知らなくても問題ありません。 このライブラリでできることはほぼ1つだけです。

open Persimmon
open Persimmon.Syntax.UseTestNameByReflection
open Persimmon.MuscleAssert

let add x y = x + y

let ``add 2 35を返す`` () = test {
  do! add 2 3 === 5
}

以上。簡単。 これを実行しても成功してしまって面白みがないので、わざと間違ってみましょう。

open Persimmon
open Persimmon.Syntax.UseTestNameByReflection
open Persimmon.MuscleAssert

let add x y = x + x // ミス!

let ``add 2 35を返す`` () = test {
  do! add 2 3 === 5
}

これをPersimmon.Consoleで実行すると、

 Assertion Violated: add 2 3が5を返す
 1. .
      left  4
      right 5

こんなエラーが出てきました。 普通ですね。

では、例えばこんなJSONがあったとしましょう。

{"widget": {
    "debug": "on",
    "window": {
        "title": "Sample Konfabulator Widget",
        "name": "main_window",
        "width": 500,
        "height": 500
    },
    "image": { 
        "src": "Images/Sun.png",
        "name": "sun1",
        "hOffset": 250,
        "vOffset": 250,
        "alignment": "center"
    },
    "text": {
        "data": "Click Here",
        "size": 36,
        "style": "bold",
        "name": "text1",
        "hOffset": 250,
        "vOffset": 100,
        "alignment": "center",
        "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
    }
}}

これを読み込む関数を定義したとして、その関数をテストしたいですよね。

let expected =

let ``JSONが読み込める`` () = test {
  do! read json === expected
}

read 関数の実装にミスがあり、textvOffsethOffset の値を使ってしまったとしましょう。 このテストを実行すると、下記のようなエラーメッセージが表示されます。

 Assertion Violated: JSONが読み込める
 1. .text.vOffset
      left  250
      right 100

textvOffset の値が左は 250 だったけど、右は 100 だった、ということが一目瞭然です。

MuscleAssert VS PowerAssert

MuscleAssertとPowerAssertの目的ははっきりと分かれています。 MuscleAssertが最初からテスティングフレームワークアサーションを書くために特化しているのに対して、PowerAssertは(テストではなく)表明に使うことを前提にデザインされています。

表明手段

表明手段としてのPowerAssertはとても便利です。 言語内蔵の assert は、条件式が false の場合に何やらメッセージを出しますが、「どこで表明が false と評価された」くらいの情報しか持っていません。 メッセージをカスタマイズすることはできますが、文字列で指定する必要があるため「どうなったか」を埋め込むのは大変です。

PowerAssertは、言語内蔵の assert をそのままに表示されるメッセージをリッチにしてくれます。 表明として埋め込んだ式の「部分式の値」がメッセージとして表示されるため、「どの式の評価値が想定と違うのか」を調べるための情報をコーディングのコストを払わずに得られるようになるのです。

対してMuscleAssertはそもそも、Persimmon.MuscleAssertはPersimmon用のライブラリとして作られているため、Persimmonに依存しており単体で使えるものではありません。 表明に使えたとしても、MuscleAssertは式全体の評価結果の差分を出すため、ほしい情報である「どの式の評価値が想定と違うのか」を調べるための情報はそこに乗っていないでしょう。

表明手段としては、PowerAssertの圧勝です。

ユニットテストアサーション

しかし、MuscleAssertがやりたかったのは表明ではありません。 ユニットテストアサーションとして使いたかったのです。

MuscleAssertが例えばJSONのようなネストした構造に対するテストに強そうだ、というのは先ほど紹介した例で分かると思います。 XMLJSONYAMLは当然として、そもそもクラス自体が何かを内部に持っているネスト構造をしているため、ネストした構造をそのまま比較してもわかりやすいメッセージが出力されるMuscleAssertは便利です。

対してPowerAssertはこの例には貧弱です。

let ``JSONが読み込める`` () = test {
  do! read json === expected
}

このテストが失敗するとして、PowerAssertで表示されるのは

  • json 変数の中身
  • read json の結果
  • expected の中身
  • read json === expectedfalse になったということ

ですかね。 どれもドバドバと大量の出力をするわりに、本当に欲しい「どこがどう違うのか?」という情報はそこから得るのは容易ではありません。 diffツールを使って外部でdiffとるとかしたことある人も多いんじゃないでしょうか?

そもそも、テストで actual 側に部分式が出てうれしいほど何かを書くことって多いのか?というのも疑問です。 このテストのように、多くのテストでは期待値との一点比較ができればいいのではないでしょうか?

ちなみに、MuscleAssertでは一度に複数の箇所の間違いを出してくれますので、小さいテストをまとめるのも容易です。

1. .image.hOffset
      left  500
      right 250
    .image.vOffset
      left  500
      right 250
    .text.vOffset
      left  250
      right 100
    .text.alignment
      left  centre
      right center

    @@ -1,6 +1,6 @@
     cent
    -re
    +er

MuscleAssertの弱点

MuscleAssertの弱点は、一点比較しかできないところです。 そのため、浮動小数点数を含むデータ構造を、浮動小数点数の一致範囲を指定して比較、ということは現状ではできません。 また、大小比較などもサポートしていません。

現状でこれらをテストしたい場合は、MuscleAssertを使わずにテストするしかありません。 今のところ、これで困ったことはありません(そういうテストが必要なドメインで仕事をしていない)。

まとめ

まとめも3行で。

  • MuscleAssert便利
  • テストのためのアサーションライブラリとしてはPowerAssertよりも便利
  • 弱点はある。でも自分が困っていないから放置

みなさんも自分が使っている言語でMuscleAssertを実装してみてはいかがでしょう?便利ですよ。

なごやかJava ゆるふわテストツール編で発表してきた

発表してきました。

テストツール編なのに、Javaにもテストツールにも関係のない、テスト自体の話です。 それなりに反応は良かったかな?

資料作ってる時に、盛り込み過ぎだったので資料から抜いたものを独立して別の資料にしたので、時間があったらそっちも発表しようかなー、と思っていたんですが、なかったので公開だけしておきます。

異論は認める。

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

タイトルはもちろん釣りで・・・はない!

ちょっと真面目に、Power Assertについて意見を述べたいのです。

そもそもPower Assertって何?

てきとーに説明すると、

普通の比較演算子で普通にassert書けば、失敗時に各部分式の値を表示してくれる

ようなものです。 Groovy製のテスティングフレームワークであるSpockがおそらく本家大本です((要出典。こういう系の発想は割と昔からあったし、Spock以前に実装例がありそうな気がする。そもそも、Spockは最初からPower Assert持ってたのかも調べないといけない。ちなみに、式木を弄ってAssertを組み立てる、というものであれば(PowerAssertよりも情報量は少なくなるものだけど)、自分の知る限りだと2009年6月にこんな記事があります。 http://themechanicalbride.blogspot.jp/2009/06/better-unit-tests-with-testassert-for.html まずはこの時点でのSpockの実装を確認せねば・・・))。

Groovyでこう書くと、

def xs = [0,1,2,3,4]
assert 1 == xs.min() 

こうなります。

Exception thrown
10 02, 2013 2:57:46 午後 org.codehaus.groovy.runtime.StackTraceUtils sanitize

WARNING: Sanitizing stacktrace:

Assertion failed: 

assert 1 == xs.min()
         |  |  |
         |  |  0
         |  [0, 1, 2, 3, 4]
         false


    at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:399)
以下略

おお!値がどうなったか一目瞭然ですね!

Power Assertをユニットテストに使う

どこがどうなったってアサーションに失敗したのかが分かりやすいため、 これをユニットテストアサーションとして採用する流れがあります。

こんな感じですね。

import groovy.transform.Canonical

@Canonical
class User {
  def name
  def age
}

def a = new User("hoge", 10)
assert a == new User("hoge", 20)
Exception thrown
10 02, 2013 3:05:37 午後 org.codehaus.groovy.runtime.StackTraceUtils sanitize

WARNING: Sanitizing stacktrace:

Assertion failed: 

assert a == new User("hoge", 20)
       | |  |
       | |  User(hoge, 20)
       | false
       User(hoge, 10)
略

Groovy知らなくても、何が起こっているのかはよくわかると思います。

何が起こっているかは、確かに一目瞭然なのですが・・・

俺たちが欲しかった情報はなんだ?

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

「どこがどうなっているか」だけ渡されても、「どこがどう違っているか」は目視で確認しなけりゃならんのです。 だるいのです。 先の例くらいならまだマシですけど、長い文字列とかだと探すの大変です。

import groovy.transform.Canonical

@Canonical
class SomeData {
  String str
  int i
}
Assertion failed: 

assert a == new SomeData("very long long long string", 20)
       | |  |
       | |  SomeData(very long long long string, 20)
       | false
       SomeData(very long long long sting, 19)

19に釣られて、very long long long stringとvery long long long stingの違いを見抜けなくて(本来)無駄なRedになってしまっても、それは仕方がないことですよね。

本当に欲しい情報って、例えばこんなものじゃないですかね?

Assertion failed:

equality check is failed.
difference:
 - SomeData.str: ["...st(-)ing", "...st(r)ing"]
 - SomeData.i: ["(19)", "(20)"]

この下に、どこがどうなったか情報があったら重宝はすると思います。 が、それが最初じゃないでしょう、と言いたいのです。

じゃぁお前が実装しろよ

ここで、「なので実装しました!」とか言えたら超かっちょいいんですけど、 (社内用テスティングフレームワークとして)作りかけて止まっちゃってます・・・
ちょっと別の色々(LangExtとか)に時間が取られちゃってまして・・・

でも、自分が欲しいのは正直こういう形の情報なんですよね。 PowerAssert的な情報は、あると便利だけどそれだけあっても辛いのです。

なので、このエントリの意見に同意してくれて、時間ある人は是非作ってみてほしいんですよね。 Power Assertに「欲しかったのはお前じゃないんだ!」を突き付けたい!!!