ラベル Visual Studio の投稿を表示しています。 すべての投稿を表示
ラベル Visual Studio の投稿を表示しています。 すべての投稿を表示

2010/01/23

MSTestExpressionAssertion - VisualStudioの単体テスト用のGallio(MbUnit)のAssertEx.Thatの改造版

VisualStudio 2008の単体テスト機能のカスタマイズ(VS2008 custom assertion example)の続き。
これも放置していたけど一段落つけるまでやった。

前のエントリで書いた通りGallioAssertEx.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指定。デバッグシンボルがないアセンブリを読み込むと警告を表示するのがデフォルトなのがうざい。対象のコードにDebuggerStepThroughAttributeDebuggerHiddenAttributeDebuggerNonUserCodeAttributeを指定しようかと思ったけどGallioのコードを変更するのを避けるため止めた。
  • でも式が連続して実行される感じではなくなっている(結果を取得するため分解して書き換えているから)のでステップ実行が微妙。まあ使えなくはない。
  • それにしてもExpressionInstrumentor.csExpressionFormattingRule.csはうまい。こんなのよく書くなと同時によく書けるなと思う。
ソースはBitBucketのMSTestExpressionAssertion
Gallioのソースからの変更部分はmodified-files.diff

2009/11/22

VisualStudio 2008の単体テスト機能のカスタマイズ(VS2008 custom assertion example)

結構前にGallioAssertEx.Thatの式ツリーの要素をキャプチャして出力できる点がすごいと思ってちょっといろいろいじっていた。Gallioは素晴らしいけどちょっと重い感じがするし、機能とか装備されているものが豊富なのはいいんだけど、もうちょっと簡単にいじれる大きすぎないフレームワークを使いたいというのが感想。
最近それをある程度の形にしようと思って、アレンジしてVisual Studio 2008の単体テスト機能で使えるようにしようとしてVisual Studio 2008の単体テスト機能のカスタマイズを調べてみた。

基本的には自前のassertionでテスト失敗の場合はAssertFailedExceptionをスローすればいいんだけど、そうするとスタックトレースに診断処理や例外をスローする処理のメソッドが含まれてしまう。
これはDebuggerHiddenAttributeDebuggerNonUserCodeAttributeを使っても避けられない。デバッガとスタックトレースは別。


スタックトレースに含まれないように回避するための属性はないみたいなので、スタックトレースを操作する方向で行こうとすると例外のスタックトレース(Exception.StackTraceプロパティとか)はStackTrace型ではなくて文字列だったりする上にSetterがない。
どうすればいいの?例外のスタックトレースは変更できないの?と思ったけどよく見るとException.StackTraceプロパティはvirtual宣言されているから派生させてオーバーライドすれば、このプロパティで返される値は変更できる。

それでこんなコードでスタックトレースに含められないようにできそう。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MSTestCustomAssertion
{
    public class ExpressionAssert
    {
        public static void IsTrue(Expression<Func<bool>> expr)
        {
            var result = expr.Compile().Invoke();
            if (!result)
                throw new FailedException("ExpressionAssert.IsTrue failed");
        }

        public class FailedException : AssertFailedException
        {
            public FailedException() : base() { }
            public FailedException(string msg) : base(msg) { }
            public override string StackTrace
            {
                get
                {
                    var lines = base.StackTrace.Split(new string[] { Environment.NewLine, }, StringSplitOptions.None);
                    var index = Array.FindIndex(lines, x => !x.Contains("ExpressionAssert."));
                    var st = string.Join(Environment.NewLine, lines, index, lines.Length - index);
                    return st;
                }
            }
        }
    }
}
ksksts / junk / source — bitbucket.org

Visual Studioの単体テスト機能でテストを失敗させた場合のスタックトレースにExpressionAssertクラスの処理が含まれない。


Visual Studioに統合された単体テストの機能以外で試してもいないし、例外クラスがAssertFailedExceptionではないという点が気になるけど、とりあえず手軽にするならこの方法でもいいかもしれない。

手間は増えるだろうけどInternalPreserveStackTraceを使ったやり方のほうがちゃんとしていそう。