これも放置していたけど一段落つけるまでやった。
前のエントリで書いた通りGallioのAssertEx.Thatで構成要素の式ををキャプチャしてその結果を出力できるのがおもしろくて同じようなことをVisualStudioの単体テストで使いたいと思ったのが動機。
いじり方としては、Gallioのソースから欲しいファイルを部分を抜き出してそのまま使ったり変更したりしてMSTestExpressionAssertion.dllという名前のアセンブリを作った。
これに含まれるAssertExクラスのIsTrueメソッドとIsFalseメソッドが、Microsoft.VisualStudio.TestTools.UnitTesting.AssertクラスのIsTrueとIsFalseの式ツリーを受け取るバージョン相当。
MSTestExpressionAssertion.dllを参照に追加して、
AssertEx.IsTrue(() => true)と書いたテストは成功して、
AssertEx.IsTrue(() => false)と書いたテストは失敗する。
テストコード:
var p = new Point(1.0, 2.0); var tole = 1.0e-12; AssertEx.IsFalse(() => Math.Abs(p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5.0)) <= tole);エラーメッセージ:
AssertEx.IsFalse failed. Math.Abs((p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5))) <= tole: True Math.Abs((p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5))): 0 p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5): 0 p.CalculateDistance(Point.ORIGIN): 2.23606797749979 p: (1, 2) Point.ORIGIN: (0, 0) Math.Sqrt(5): 2.23606797749979 tole: 1E-12まあこんなものかと。 式の中でのリテラルが浮動小数点数ではなくなっていたり、結果の出力はToString()メソッドにしているせいでそのあたりにも少し不満があるけど。
テストコード:
var m = Matrix.MakeRotation(Math.PI * 0.25); var tole = 1.0e-12; AssertEx.IsFalse(() => m.Transform(new Point(1.0, 0.0)).CalculateDistance(new Point(Math.Sqrt(2.0) * 0.5, Math.Sqrt(2.0) * 0.5)) <= tole);エラーメッセージ:
AssertEx.IsFalse failed. m.Transform(new Point(1, 0)).CalculateDistance(new Point((Math.Sqrt(2) * 0.5), (Math.Sqrt(2) * 0.5))) <= tole: True m.Transform(new Point(1, 0)).CalculateDistance(new Point((Math.Sqrt(2) * 0.5), (Math.Sqrt(2) * 0.5))): 1.11022302462516E-16 m.Transform(new Point(1, 0)): (0.70710678118654757, 0.70710678118654746) m: (0.70710678118654757, 0.70710678118654757, 0.70710678118654746, 0) new Point(1, 0): (1, 0) new Point((Math.Sqrt(2) * 0.5), (Math.Sqrt(2) * 0.5)): (0.70710678118654757, 0.70710678118654757) Math.Sqrt(2) * 0.5: 0.707106781186548 Math.Sqrt(2): 1.4142135623731 Math.Sqrt(2) * 0.5: 0.707106781186548 Math.Sqrt(2): 1.4142135623731 tole: 1E-12この程度で既にうるさく感じる。 "Math.Sqrt(2) * 0.5"(とその下の"Math.Sqrt(2)")が2回出力されるのもどうかと思うけど、オブジェクトが変更されるケースも当然あるから同一の式&結果ならまとめるとかいうのもあまりよくない気がする。 細かいけどデフォルトのフォント設定ではmの結果のインデントがずれる(結果が複数行の場合のインデント処理をせっかく入れたのに)。
例外をスローするテストコード:
AssertEx.IsTrue(() => string.Format("{0}", null) == "");エラーメッセージ:
テスト メソッド MSTestExpressionAssertionTest.AssertExSample.Test02 は例外をスローしました: System.ArgumentNullException: 値を Null にすることはできません。 パラメータ名: args。スタックトレース:
System.String.Format(IFormatProvider provider, String format, Object[] args) lambda_method(ExecutionScope ) Gallio.Common.Linq.ExpressionInstrumentor.Intercept[T](Expression expr, Func`1 continuation) Intercept[T](Expression expr, Func`1 continuation) Gallio.Common.Linq.ExpressionInstrumentor.InterceptNonVoid[T](Expression expr, Func`1 continuation) lambda_method(ExecutionScope ) Gallio.Common.Linq.ExpressionInstrumentor.Intercept[T](Expression expr, Func`1 continuation) Intercept[T](Expression expr, Func`1 continuation) Gallio.Common.Linq.ExpressionInstrumentor.InterceptNonVoid[T](Expression expr, Func`1 continuation) lambda_method(ExecutionScope ) Eval(Expression`1 condition) MSTestExpressionAssertion.AssertEx.IsTrue(Expression`1 condition, String message, Object[] parameters) MSTestExpressionAssertion.AssertEx.IsTrue(Expression`1 condition) MSTestExpressionAssertionTest.AssertExSample.Test02() C:\home\development\projects\bitbucket\junk\cs\MSTestExpressionAssertion\MSTestExpressionAssertionTest\AssertExSample.cs 内: 行 42スローされた例外クラスはSystem.ArgumentNullExceptionクラス。
例外クラスの型とスタックトレースの両方をうまく維持する方法が分らなかった(というか多分ない)から、スローされる例外クラスを優先した。そのためスタックトレースにMSTestExpressionAssertionのコードも含まれてしまっている。
Gallioからの主な変更内容:
- 式ツリーに含まれる定数式以外のすべての式とその結果を出力するようにした
- 式の中で発生した例外はcatchせずにそのまま挙げるようにした
- 結果の出力はToString()メソッドを使うようにした
- すべての式を出力するための式のフォーマッタが面倒だった。GallioのExpressionFormattingRule.csをベースにしたExpressionFormatter.cs(と補助的にExpressionExtensions.cs)がその部分。staticメンバやthisのメンバの判定処理についてはもっとうまい方法があるかも。
- デバッガで楽にテストの式にステップインできるようにMSTestExpressionAssertionプロジェクトのReleaseビルドでは/debug:none指定。デバッグシンボルがないアセンブリを読み込むと警告を表示するのがデフォルトなのがうざい。対象のコードにDebuggerStepThroughAttributeやDebuggerHiddenAttributeやDebuggerNonUserCodeAttributeを指定しようかと思ったけどGallioのコードを変更するのを避けるため止めた。
- でも式が連続して実行される感じではなくなっている(結果を取得するため分解して書き換えているから)のでステップ実行が微妙。まあ使えなくはない。
- それにしてもExpressionInstrumentor.csとExpressionFormattingRule.csはうまい。こんなのよく書くなと同時によく書けるなと思う。
Gallioのソースからの変更部分はmodified-files.diff。