仕様は、テストデータでテストする必要があるものを記述するテキストファイルです。 プログラムが受け取るべき結果を示します。 テストコードは、ライブコード結果で計算された実際のコードを見つけます。 テストエンジンは、仕様と計算結果を調整します。
このアプローチにより、宣言的にテストを作成できます。 仕様は読みやすく、要件が変わったときに補足します。 テストコードはコンパクトです。 保守と拡張が簡単です。
この記事では、仕様をテストするためのエンジンの原理について説明し、使用例を示します。 エンジン自体は記事に添付されます。 統合テスト用の小さなライブラリと見なすことができます。
ライブラリコード
ソースコード-stalker98.narod.ru/TestBySpecification.rar
コードは詳細にコメントされているため、記事を読んだ後もいくつかの点が不明確なままである場合、実装で詳細を確認できます。 概して、ライブラリは4つのクラスのみで構成されていますが、これでは十分ではありません。 テストエンジンはUtils.Common.Testsにあります。
テストには、test studio 2008フレームワークを使用しますが、NUnitに移植できます。 これを行うには、Test.Domain.Documents.DocumentTester.csおよびTest.Domain.Polygons.PolygonTester.csのテスト呼び出しを修正する必要があります。
例のサブジェクトエリア
あるフォーマットから別のフォーマットへのドキュメントコンバーターの作成をタスクとします。 変換は、多くの数学的な計算を伴うトリッキーです。 変換の最も難しい部分は、ドキュメントに記載されている幾何学的形状の処理です。 顧客は、別の形式に転送する必要がある標準ドキュメントのセットを引き渡しました。
開発はまだ遠いです。 すでに記述されたコードはユニットテストで覆われており、各テストはモジュールを他のプログラムから隔離してテストします。 ただし、記述されたコードが実際のデータで機能することを確認する必要がある場合があります。 この場合のプログラムクラスは、互いに分離せずに一緒に使用する必要があります。 統合テストを実施する必要があります。
テストへのアプローチはこれです-顧客から送られた標準文書ごとに、変換時にプログラムが得る重要な結果を書き留める仕様を作成します。 送信されたドキュメントから、さまざまな図を選択し、それらの仕様も記述します。 図の仕様では、特定の図を処理するときにプログラムが到達すべき結果を書き留めます。
仕様ファイル
仕様は、テストデータでテストする必要のあるものを記述したテキストファイルです。 ファイルの構成は次のとおりです。
- ファイルの先頭のコメント。 コメントは、$以外の任意の文字で構成できます。
- チェックされたプロパティ。 各プロパティの名前は$で始まります。 プロパティ値の終わりは、次のプロパティの始まりまたはファイルの終わりによって決定されます。
4 3
|
テストコード
仕様からのプロパティの適合性と実際の(計算された)プロパティ値を確認するには、これらの実際のプロパティ値を見つける方法を知る必要があります。 これを行うために、仕様クラスの継承者が作成され、仕様内と同じ名前でプロパティの取得アクセサがそのクラスに書き込まれます。
public class PolygonSpecification : Specification
{
/// <summary>
///
/// </summary>
private int VerticeCount
{
get { return Polygon.Vertices.Count( ); }
}
/// <summary>
///
/// </summary>
private bool IsRhombus
{
get { return Polygon.IsRhombus; }
}
/// <summary>
///
/// </summary>
private bool HasSelfIntersections
{
get { return Polygon.SelfIntersections.Any( ); }
}
// ...
}
プロパティのタイプは名前と一致する必要があります。 たとえば、Is、Has、またはAreで始まるプロパティは、ライブラリによってブールフラグと見なされます。 そして、Countで終わるプロパティは整数です。 プロパティ名とそのタイプを一致させるメカニズムを以下に説明します。
ご覧のとおり、テストコードは最小化されています。
テスト通話
[ TestClass ( )]
public class PolygonTester : Engine < PolygonSpecification >
{
/// <summary>
/// PolygonSpecification
/// </summary>
[ TestMethod ( )]
public void Polygon_AllSpecifications( )
{
Assert .IsTrue( TestSpecifications( ), FailedSpecInfo );
}
/// <summary>
///
/// </summary>
[ TestMethod ( )]
public void Polygon_DebugSpecification( )
{
Assert .IsTrue( TestSpecification( "" ), FailedSpecInfo );
}
}
スタジオでのテストの開始Ctrl + R、A:
テスト出力
テスト中、操作はログに記録されます(テスト出力でもあります)。 テストされた仕様とプロパティはログに書き込まれ、テストの結果は何で、テストにはどれくらい時間がかかりましたか。 一部のプロパティがテストに合格していない場合、これは個別に示されます。 [テスト結果]ウィンドウでテストをクリックすると、テストの出力を確認できます。
さまざまな形状の仕様をさらに5つ作成しました。 テスト時に、仕様のIsRhombusプロパティ(ブールフラグ-数字が菱形かどうか)がプログラム内の値と一致しないことが判明しました。 明らかに、図が菱形かどうかを判断する方法に間違いがありました。 ログは次のようになります。
PolygonTester
|
プロパティ定義メカニズム
現時点で注意深い読者にとって、2つの質問は明確ではないはずです。テストデータはどこから来て、2つのブールまたは整数の等値以外をテストする必要がある場合はどうすればよいでしょうか。 両方の質問に対する答えは、プロパティを決定するメカニズムにあります。
各プロパティは、1つの特定のタイプを参照します。 さらに、ここでの型は、単なる.NET型以上のものを意味します。 むしろ.NETタイプに加えてテスト動作です。 動作は、特別なオブジェクト-プロパティ記述子PropertyDescriptor <T>によって記述されます。 記述子は、プロパティ名に採用される規則、文字列からプロパティ値への変換、およびその逆、およびプロパティがテストに合格したかどうかを判断する基準を定義します。
ブールフラグのライブラリ記述子の例を示します。
protected PropertyDescriptor < bool > FlagProperty = new PropertyDescriptor < bool >
{
NamePattern = @"(Is|Has|Are)\w+" ,
Convert = ( value ) => bool .Parse( value ),
Verify = ( specified, actual ) => specified == actual,
};
記述子は、正規表現NamePatternを指定します。これは、記述子が適用されるプロパティの命名規則を設定します。 この場合、これらはすべてIs、Has、またはAreで始まる名前のプロパティです。 Convertは、文字列からプロパティ値への変換関数を定義します。 検証は、テストに合格するための基準を定義します。 この場合、これは単純な同等性テストです。 つまり、仕様のブール値が計算値と等しい場合、プロパティがテストに合格したと見なされます。 Verifyを省略すると、プロパティは読み取られますが、テストされません。 プロパティの値を文字列に変換するTranslateもあります。 指定しない場合、ロギング時にToString()が使用されます。
ブールフラグ記述子に加えて、ライブラリには整数カウント(Countで終わるプロパティ)の事前定義された記述子があります。 これは、上記で解析した記述子と非常に似ているため、解析で読者を退屈させません。
新しいタイプの仕様拡張
ここまで、図の仕様の例を示しました。 しかし、まだドキュメントの仕様があります。 次の状況を想像してください-システムの予備的なショーで、プログラムがドキュメントを誤って解析することが判明しました。 キリル文字の代わりに、正方形が表示されます。 このようなエラーが今後発生しないように、テストを作成します。 エラーが検出されたドキュメントの仕様を、セクションの名前をリストする行で補足します。
$SectionNames = , ,
|
/// <summary>
///
/// </summary>
private IEnumerable < string > SectionNames
{
get { return Document.Sections.Select( s => s.Name ); }
}
テストを開始できましたが、エラーでクラッシュします。 ライブラリは、このタイプのプロパティを処理する方法を知りません。 ライブラリが認識している記述子はどれも適切ではありません-プロパティの名前がフラグまたはカウンターのいずれとも一致しません。 新しい記述子を作成する必要があります。 テストコードで宣言します-これはライブラリがそれを見つけるのに十分です:
/// <summary>
///
/// </summary>
protected PropertyDescriptor < IEnumerable < string >> SectionNamesProperty = new PropertyDescriptor < IEnumerable < string >>
{
NamePattern = @"SectionNames" ,
Convert = ( text ) => text.Split( ',' ).Select( n => n.Trim( ) ),
Verify = ( specified, actual ) =>
specified.Count( ) == actual.Count( ) &&
specified.Intersect( actual ).Count( ) == actual.Count( ),
Translate = ( value ) => string .Format( "[{0}]: {1}" ,
value .Count( ),
string .Join( ", " , value .ToArray( ) ) ),
};
上記の記述子は、「SectionNames」という名前のプロパティに適用されます。 仕様から読み取られた行をコンマ区切りの行に変換し、極端なスペースを削除します。 検証は、文字列の2つのコレクションが同等であるときにプロパティがテストに合格することを決定します。最初のコレクションの各要素が2番目のコレクションに存在し、その逆も同様です。 検証が失敗した場合に、匿名タイプの名前ではなく、意味のある碑文がログに表示されるように、翻訳が必要です。 Translateは、コレクション内の要素の数が示され、それらの値がコンマでリストされる行を作成します。
記述子は入力されるため、IntelliScenseが入力されると、引数のタイプが要求され、コンパイラーは操作の正確性をチェックします。
将来、同じ動作のプロパティを追加する必要がある場合、NamePattern記述子で@ "\ w + Names"に変更できます。 そして、名前で終わるすべてのプロパティはこの記述子を使用します。
テストデータの読み取り
テストデータはどこにでも配置できます-ライブラリは制限を課しません。 ただし、実際には、テストデータを格納するために2つの場所を使用すると便利であることが判明しました。仕様自体または別のファイルです。 どちらの場合も、ファイルは埋め込みリソースとしてテストプロジェクトDLLに保存されます。 これにより、次のことが可能になります。
- バージョン管理システムのすべてのデータとともに統合テストを含めます。
- スタジオで作業するときは、常に仕様とテストデータを手元に用意してください。
1)テストデータは仕様に縫い付けられています
初期化するのに大量のデータを必要としないオブジェクトのテストに便利です。 記述子は仕様クラスで宣言されており、Verifyメソッドは指定されていません。 プロパティの値は、SpecifiedProperties仕様の読み取りプロパティの辞書から取得されます。
/// <summary>
/// ,
/// </summary>
PropertyDescriptor < IEnumerable < Vector >> VerticesProperty = new PropertyDescriptor < IEnumerable < Vector >>
{
NamePattern = "Vertices" ,
Convert = ( text ) => Polygon .ParseVertices( text ),
};
/// <summary>
///
/// </summary>
public Polygon Polygon
{
get { return new Polygon ( ( IEnumerable < Vector >) SpecifiedProperties[ "Vertices" ] ); }
}
2)外部ファイルのテストファイル
大量のデータがある場合、またはテストデータがドキュメントである場合に適しています。 仕様ファイルとテストデータの名前が一致する契約を受け入れると便利です。 同時に、テストデータは仕様に関連する隣接ディレクトリにあります。 次に、読み取り値は次のようになります。
/// <summary>
///
/// </summary>
public Document Document
{
get
{
var assembly = Assembly .GetExecutingAssembly( );
var resourceName = string .Format(
"{0}.Documents.Data.{1}.txt" ,
assembly.GetName( ).Name, Name );
var stream = assembly.GetManifestResourceStream( resourceName );
var text = new StreamReader ( stream ).ReadToEnd( );
return Document .CreateFromText( text );
}
}
受け入れられた契約
テストエンジンは、次の規則に依存しています。
- 仕様のタイプごとに、テストプロジェクトに個別のフォルダーが作成されます。
- 仕様クラスからの継承者がこのフォルダーに作成されます。
- 仕様ファイルは、埋め込みリソースとしてSpecsサブフォルダーに追加されます。
- 仕様ファイルの拡張子は.specでなければなりません
- 仕様は、テストが開始された同じアセンブリで検索されます。 ただし、必要に応じて、Engine.Assemblyでアセンブリを明示的に指定できます。
テストデータを個別のファイルに保存する場合、構造は次のようになります(仕様などのデータは、埋め込みリソースとしてアセンブリに保存されます)。
仕様では:
- プロパティ名は、テストコードの計算されたプロパティと一致する必要があります。
- 読み取られる各プロパティには、正確に1つの記述子が必要です。
- テストコードでは、プロパティがテストされる順序を想定しないでください。 テストは相互に影響しないようにする必要があります。
おわりに
前回のプロジェクトで統合テストに上記のアプローチを適用し、結果に満足しました。 仕様ベースのテストは、単体テストとして実装された同様のテストと比較して、読みやすく保守しやすいです。
実際、仕様はMini DSL(ドメイン固有言語)で記述されたファイルです。 ライブラリはこの言語のエンジンであり、テストされたコードと対話するためのAPIを定義します。 汎用言語(C#)では、仕様テストも記述できますが、これにより読みやすさが低下し、テストをサポートするコストが増加します。
将来、プロパティに計算する権利がある時間を示す機能をライブラリに追加すると思います。 その後、テストに割り当てられた時間を制限し、SLA(System Level Agreements)契約を確認することができます。