プリリリースです!
Release Prig v0.0.0-alpha - urasandesu/Prig - GitHub
前回から少し時間が空いてしまいましたが、リリースに際して必要な機能が一通り実装できたこともあり、プリリリースする運びとなりました。
後は、リリースまでテスト!ドキュメント!サンプル!テスト!ドキュメント!サンプル!テスト!ドキュメント!サンプル!と、ひたすら仕上げ作業を行うことになると思います。
諸々の事情で、前回解説した使い方からはだいぶ変わってしまってますので、ご承知おきを。追加した機能もざっくりとですが、一通り紹介させていただければと思います。では、行ってみましょ!
※まだα版ということもありますので、それを頭の片隅に置いてご笑覧いただければと思いますですー m(_ _)m
以下の記事、ライブラリを使用/参考にさせていただいています。新機能となると、本当に先人のお知恵が身に沁みますね・・・。多謝! (`・ω・́)ゝ
Zero to Hero: Untested to Tested with Microsoft Fakes Using Visual Studio | TechEd North America 2014 | Channel 9
Test Isolation Is About Avoiding Mocks — Destroy All Software Blog
Microsoft Fakesを使ったVisualStudio単体テストをJenkinsで実行する blog.prvten.com
Expression Trees をシリアライズする - TAKESHIK.ORG
Powershell script from Visual Studio Post-build-event failing - Stack Overflow
Getting code coverage from your .NET testing using OpenCover. - CodeProject
Visual Studio Test Tooling Guides - Downloads
NCover Moles coverage support
Mocking and Isolation Frameworks Deep Dive in .NET - Roy Osherove
Profilers, in-process side-by-side CLR instances, and a free test harness - David Broman's CLR Profiling API Blog
Why do assemblies with the SecurityTransparent attribute cause instrumented code via a profiler to throw a VerificationException? - Stack Overflow
Building NUnit on Windows 8.1 - nunit-dev Wiki
Moq/moq - GitHub
Cheat Sheet - AutoFixture/AutoFixture Wiki - GitHub
c# - .Net Fakes - How to shim an inherited property when the base class is sealed? - Stack Overflow
Real time unit testing - or "how to mock now" - Programmers Stack Exchange
Microsoft Fakes; Testing the Untestable Code
sawilde/opencover - GitHub
Microsoft Fakes Framework—SVNUG Presentation 35 - YouTube
Write MSIL Code on the Fly with the .NET Framework Profiling API
ReJIT Limitations in .NET 4.5 - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs
Walking the stack of the current thread | Johannes Passing's Blog
slimtune - A free profiling and performance tuning tool for .NET applications - Google Project Hosting
c# - How to stub 2nd call of method? - Stack Overflow
Four Ways to Fake Time, Part 4 | Ruthlessly Helpful
Advanced Usage | JustMock Documentation
Generics and Your Profiler - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs
目次
クイックツアー
試作版スタブ生成器の追加やランナーの追加で導入手順はかなり変わりました。前回も登場した、「テストしにくい副作用を直接参照している処理」を例に見ていきます:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace program1.MyLibrary | |
{ | |
public static class LifeInfo | |
{ | |
public static bool IsNowLunchBreak() | |
{ | |
var now = DateTime.Now; | |
return 12 <= now.Hour && now.Hour < 13; | |
} | |
} | |
} |
手順としては以下のような感じ:
Step 1: スタブ設定の作成
Step 2: スタブの生成
Step 3: テストの作成
Step 4: テストの実行
Final Step: リファクタリングしてキレイに!
では、実際にやってみたいと思います。
Step 1: スタブ設定の作成
以下のようなスタブ設定を作成します:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<configuration> | |
<configSections> | |
<section name="prig" type="Urasandesu.Prig.Framework.PilotStubberConfiguration.PrigSection, Urasandesu.Prig.Framework" /> | |
</configSections> | |
<!-- | |
タグ 'RuntimeMethodInfo' の中身は、NetDataContractSerializer.WriteObject(から、アセンブリバージョンを抜いたもの)で生成されています。 | |
具体的には、以下の PowerShell スクリプトで生成することができます: | |
========================== 例 ========================== | |
PS C:\> $methods = @([Type]::GetType('System.DateTime').GetMethods(([System.Reflection.BindingFlags]'Public, NonPublic, Static, Instance')) | ? { $_.Name -eq 'get_Now' }) | |
PS C:\> $methods | % { $_.ToString() } | |
System.DateTime get_Now() | |
PS C:\> $methods[0] | & .\Invoke-NetDataContractSerializer.ps1 | & clip | |
PS C:\> | |
生成したら、'add' タグの間にクリップボートの中身を貼り付けてくだしあ。 | |
--> | |
<prig> | |
<stubs> | |
<!-- | |
<add name="$(この属性は識別子です。メソッドのオーバーロードが識別できるようなものが望ましいです。メソッドが 1 つのシグネチャしか持たないのであれば、'alias' と同じで構いません。)" | |
alias="$(この属性は、'name' を指定するエイリアスです。テストコードで使うのはこちらになります。)"> | |
$('Invoke-NetDataContractSerializer.ps1' の結果をここに貼り付けてください。) | |
</add> | |
--> | |
<add name="NowGet" alias="NowGet"> | |
<RuntimeMethodInfo xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:x="http://www.w3.org/2001/XMLSchema" z:Id="1" z:FactoryType="MemberInfoSerializationHolder" z:Type="System.Reflection.MemberInfoSerializationHolder" z:Assembly="0" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns="http://schemas.datacontract.org/2004/07/System.Reflection"> | |
<Name z:Id="2" z:Type="System.String" z:Assembly="0" xmlns="">get_Now</Name> | |
<AssemblyName z:Id="3" z:Type="System.String" z:Assembly="0" xmlns="">mscorlib</AssemblyName> | |
<ClassName z:Id="4" z:Type="System.String" z:Assembly="0" xmlns="">System.DateTime</ClassName> | |
<Signature z:Id="5" z:Type="System.String" z:Assembly="0" xmlns="">System.DateTime get_Now()</Signature> | |
<Signature2 z:Id="6" z:Type="System.String" z:Assembly="0" xmlns="">System.DateTime get_Now()</Signature2> | |
<MemberType z:Id="7" z:Type="System.Int32" z:Assembly="0" xmlns="">8</MemberType> | |
<GenericArguments i:nil="true" xmlns="" /> | |
</RuntimeMethodInfo> | |
</add> | |
</stubs> | |
</prig> | |
</configuration> |
パッケージの中に "Urasandesu.Prig.Framework\PilotStubber.prig.template" としてテンプレートがありますので、そちらも参考にされると良いかと思います。
Step 2: スタブの生成
開発者コマンド プロンプト for VS2013 を実行し、スタブ生成のために PowerShell スクリプト "Invoke-PilotStubber.ps1" を実行します:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CMD Test.program1>cd | |
C:\Prig\Test.program1 | |
CMD Test.program1>"%windir%\system32\WindowsPowerShell\v1.0\powershell.exe" -Version 2.0 -NoLogo -NoProfile | |
PS Test.program1> $ReferenceFrom = @("C:\Prig\Release(.NET 3.5)\AnyCPU\Urasandesu.Prig.Framework.dll") | |
PS Test.program1> $Assembly = "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" | |
PS Test.program1> $TargetFrameworkVersion = "v3.5" | |
PS Test.program1> $KeyFile = "C:\Prig\Test.program1\Test.program1.snk" | |
PS Test.program1> $OutputPath = "C:\Prig\Test.program1\bin\Release(.NET 3.5)\x86\" | |
PS Test.program1> $Settings = "C:\Prig\Test.program1\mscorlib.prig" | |
PS Test.program1> & "C:\Prig\Release(.NET 3.5)\AnyCPU\Invoke-PilotStubber.ps1" -ReferenceFrom $ReferenceFrom -Assembly $Assembly -TargetFrameworkVersion $TargetFrameworkVersion -KeyFile $KeyFile -OutputPath $OutputPath -Settings $Settings | |
Microsoft (R) Build Engine Version 12.0.30110.0 | |
[Microsoft .NET Framework, Version 4.0.30319.34014] | |
Copyright (C) Microsoft Corporation. All rights reserved. | |
Build started 2014/05/18 16:22:51. | |
Project "C:\Prig\Test.program1\mscorlib.v2.0.50727.v2.0.0.0.x86.Prig\mscorlib.Prig.g.csproj" on node 1 (rebuild target(s)). | |
CoreClean: | |
... | |
Done Building Project "C:\Prig\Test.program1\mscorlib.v2.0.50727.v2.0.0.0.x86.Prig\mscorlib.Prig.g.csproj" (rebuild target(s)). | |
Build succeeded. | |
0 Warning(s) | |
0 Error(s) | |
Time Elapsed 00:00:00.35 | |
PS Test.program1> exit | |
CMD Test.program1> | |
いちいち打ち込むのは大変ですので、実際は、*.csproj のビルド前イベント等にスクリプトを埋め込むのをオススメします。.NET のバージョンやプロセッサアーキテクチャ毎の構成を作る場合は、パッケージの中の "Test.program1\Test.program1.csproj" も参照していただければと。
Step 3: テストの作成
単体テストのための新しいクラスライブラリを作成し、スタブ Dll を参照に加えます。
テストコードでは、スタブを利用し、偽の情報を返すテストダブルに置き換えることで、テストが可能になるのです!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NUnit.Framework; | |
using program1.MyLibrary; | |
using System; | |
using System.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace Test.program1.MyLibraryTest | |
{ | |
[TestFixture] | |
public class LifeInfoTest | |
{ | |
[Test] | |
public void IsNowLunchBreak_should_return_false_when_11_oclock() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
PDateTime.NowGet.Body = () => new DateTime(2013, 12, 13, 11, 00, 00); | |
// Act | |
var result = LifeInfo.IsNowLunchBreak(); | |
// Assert | |
Assert.IsFalse(result); | |
} | |
} | |
[Test] | |
public void IsNowLunchBreak_should_return_true_when_12_oclock() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
PDateTime.NowGet.Body = () => new DateTime(2013, 12, 13, 12, 00, 00); | |
// Act | |
var result = LifeInfo.IsNowLunchBreak(); | |
// Assert | |
Assert.IsTrue(result); | |
} | |
} | |
[Test] | |
public void IsNowLunchBreak_should_return_false_when_13_oclock() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
PDateTime.NowGet.Body = () => new DateTime(2013, 12, 13, 13, 00, 00); | |
// Act | |
var result = LifeInfo.IsNowLunchBreak(); | |
// Assert | |
Assert.IsFalse(result); | |
} | |
} | |
} | |
} |
Step 4: テストの実行
本来は、プロファイラベースのモックツールを有効にするためには、環境変数を弄る必要があります。Microsoft Fakes/Typemock Isolator/Telerik JustMock は、そのための小さなランナーを提供しますので、Prig でも同様としました。なので、テストを実行するには "prig.exe" を以下のように使用する必要があります:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CMD x86>cd | |
C:\Prig\Test.program1\bin\Release(.NET 3.5)\x86 | |
CMD x86>"..\..\..\..\Release\x86\prig.exe" run -process "C:\Program Files (x86)\NUnit 2.6.3\bin\nunit-console-x86.exe" -arguments "Test.program1.dll /domain=None" | |
NUnit-Console version 2.6.3.13283 | |
Copyright (C) 2002-2012 Charlie Poole. | |
Copyright (C) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov. | |
Copyright (C) 2000-2002 Philip Craig. | |
All Rights Reserved. | |
Runtime Environment - | |
OS Version: Microsoft Windows NT 6.2.9200.0 | |
CLR Version: 2.0.50727.8000 ( Net 3.5 ) | |
ProcessModel: Default DomainUsage: None | |
Execution Runtime: net-3.5 | |
.......... | |
Tests run: 10, Errors: 0, Failures: 0, Inconclusive: 0, Time: 1.44990184322627 seconds | |
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0 | |
CMD x86> |
Final Step: リファクタリングしてキレイに!
テストができてしまえば、リファクタリングに躊躇はなくなりますね!例えば、以下のようにリファクタリングできることがわかると思います:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace program1.MyLibrary | |
{ | |
public static class LifeInfo | |
{ | |
public static bool IsNowLunchBreak() | |
{ | |
return DateTime.Now.Hour == 12; // こっちの方が良いかな? | |
} | |
} | |
} |
こんな感じで、Prig はテストしにくいライブラリに依存するようなコードをキレイにするのをサポートしてくれます。開発がまた楽しくなること請け合いですよ!
パフォーマンスも含め、使い勝手についてはまだ大いに改善する余地があると思います。特にスタブ生成周りはヒドイ・・・。もともと、こんな感じになってしまうことは分かっていましたので、「手動でスタブ作って ildasm 見てメタデータトークン埋め込んだほうが楽じゃね?」と思い、前回紹介させていただいたような方式を採っていたのですが、メタデータトークン直書きだと、Windows Update でヤられてしまうことがわかったのでした・・・。プリリリースに当たり、急遽試作版スタブ生成器を導入したわけですが、パターンが出し切れていないことから、使い勝手より柔軟性を重視する方向に振っています。このパターン、というのは例えば、sealed なクラスが持つ abstract な親の副作用付プロパティや、I/F に internal なアクセス修飾子の型が現れるメソッド、Fakes ですらリリース時にはサポートできていなかったジェネリック型制約など、ですね。最終的には、IL 直吐き型のスタブ生成器にする予定ですが、まだちょっと検討不足。ぼちぼちと考えていこうと思います。
元のメソッド呼び出しのサポート
迂回処理を書いていくと、テストケースによっては、「元の処理はそのまま呼び出して、引数に渡ってくるものを検証したい」とか、「n 回目以降だけダミーに入れ替えたい」とか、の要望は出てくると思います。そのためのガード機能が、IndirectionsContext.ExecuteOriginal。Fakes で言う ShimsContext.ExecuteWithoutShims と同じ機能です。
「元の処理は~」の方は、Fakes を使った単体テストの指南書、「Better Unit Testing with Microsoft Fakes」にも、「Validating (private) Implementation Details」(実装の詳細の確認)として例が挙げられていますが、ちょっと難しそうに見えましたので、もう少し簡単なサンプルで見てみます:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Collections.Generic; | |
using UntestableLibrary; | |
namespace program1.MyLibrary | |
{ | |
public class Village | |
{ | |
public Village() | |
{ | |
var r = new Random(DateTime.Now.Second); // !?!? | |
var count = r.Next(100); | |
for (int i = 0; i < count; i++) | |
m_ricePaddies.Add(new RicePaddy(i, r)); | |
for (int i = 0; i < count; i++) | |
{ | |
for (int j = i + 1; j < count; j++) | |
{ | |
var distance = r.Next(100); | |
m_roads.Add(new FarmRoad(m_ricePaddies[i], m_ricePaddies[j], distance)); | |
m_roads.Add(new FarmRoad(m_ricePaddies[j], m_ricePaddies[i], distance)); | |
} | |
} | |
} | |
・・・ |
Village(村)オブジェクトは、生成するとランダムに 100 個未満の RicePaddy(田んぼ)とそれを繋ぐ FarmRoad(農道)を生成します。なお農道には、その距離として distance も設定されます。
あれれ?早速疑問が。Random の初期化は、デフォルトコンストラクタ(Environment.TickCount)ではなく、わざわざ Seed を指定するコンストラクタを使って DateTime.Now.Second を突っ込んでいますね。これでは簡単に同じ値になってしまい、同じ結果しか生まない村がたくさんできてしうまうはず・・・村ののんびりした雰囲気を出すためでしょうか?いやいやいやいや・・・。
まあ、これぐらいの規模であればえいやっと直してしまっても、レビューアーに突き返されることは無いでしょうが、大きくなってくるとテストを書き、仕様が囲えたほうが安心できますね(Fakes の資料も、実は効果を見せるために、わざと難しい例にしているのかもしれません!)。サンプルということでとりあえず書いてみましょう。こんな感じで:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
・・・ | |
[Test] | |
public void Constructor_shall_not_call_within_short_timeframe_to_generate_unique_information() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var seeds = new HashSet<int>(); | |
PRandom.Constructor.Body = (@this, seed) => | |
{ | |
IndirectionsContext.ExecuteOriginal(() => | |
{ | |
var ctor = typeof(Random).GetConstructor(new[] { typeof(int) }); | |
ctor.Invoke(@this, new object[] { seed }); | |
}); | |
seeds.Add(seed); | |
}; | |
new Random(); // JIT の準備 | |
seeds.Clear(); | |
// Act | |
var vil1 = new Village(); | |
Thread.Sleep(TimeSpan.FromSeconds(1)); | |
var vil2 = new Village(); | |
// Assert | |
Assert.AreEqual(2, seeds.Count); | |
} | |
} | |
・・・ |
スタブの埋め込み時間がまだバカにならないため、Fakes のサンプルと異なり、あらかじめ JIT をしておく必要があったり(new Random(); の部分ですね)、短い時間に間引くことが難しかったり(Thread.Sleep(TimeSpan.FromSeconds(1)); の部分ですね)するのですが、意図することは大体同様にできるはずです。
仕様が囲えたら、Thread.Sleep(TimeSpan.FromSeconds(1)); の部分をコメントアウトして、テストが失敗することを確認しましょう。これで安心して不具合修正ができますね!(そうそう、Environment.TickCount 案は、採用しても、失敗したテストを通すことができないことにすぐ気づくと思います。実はこのカウンタ、あまり精度良くないのですよね。簡単に見えて、実は落とし穴がある例でした Ψ(`∀´)Ψ)
ちょっと注意が必要なのが構造体。迂廻処理に、クラスと同じ取り回しで引数を与えると、コピーしたものに対する処理しかできなくなってしまいますので、シグネチャが参照渡しに変わっています。例えば、以下のようなコードがあって、:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace program1.MyLibrary | |
{ | |
public class RicePaddy | |
{ | |
internal RicePaddy(int identifier, Random r) | |
{ | |
Identifier = identifier; | |
var yield = r.Next(); | |
m_yield = yield % 10 == 0 ? default(int?) : yield * 1000; | |
} | |
・・・ |
コンストラクタでどのように Nullable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
・・・ | |
[Test] | |
public void Constructor_should_be_initialized_by_non_null_if_number_that_is_not_divisible_by_10_is_passed() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var actualValue = 0; | |
PRandom.Next.Body = @this => 9; | |
PNullable<int>.Constructor.Body = (ref Nullable<int> @this, int value) => | |
{ | |
actualValue = value; | |
@this = IndirectionsContext.ExecuteOriginal(() => new Nullable<int>(value)); | |
}; | |
// Act | |
var paddy = new RicePaddy(1, new Random()); | |
// Assert | |
Assert.AreEqual(9000, actualValue); | |
} | |
} | |
・・・ |
のようになります。第一引数を置き換える感じで初期化をするんですね。ちなみに、私が探す限り、Fakes だと同じことができないようなのですが・・・まあ、もし本当に無かったとしても、構造体にこういうものが欲しくなることはめったにない、という判断なのでしょう。
ジェネリックのサポート
.NET の IL 周りをイチからやられている方であれは、きっと誰もが気絶しそうになることを想像に易いこの作業。
一度やってみると、「リフレクション API って、なんて抽象化されてて使いやすいんだ!」と感じること請け合いです。IL のような、ある程度抽象化された中間言語でもここまで難しいのだから、と、某 Java のジェネリックで型情報が消えてしまう件への悲しみが和らいだり、なぜ某 TypeScript が v0.9 でコンパイラをイチから書き直さなければならなかったのかの妄想が捗ったり、ますます某 C++ のテンプレートに対する畏怖の念が強まったりするかもしれません。
リフレクション API の話題が出たところでちょっと脱線しますが、最初に挙げていた要件を満たしつつ、最悪私一人のリソースでも保守して行けるような規模に抑えるためには、アンマネージで、かつリフレクション API や、Mono.Cecil に匹敵する使い勝手・抽象度の機能群を準備することは不可欠でした。
これに当たるのが、Prig のサブモジュールにもなっている Swathe です。
urasandesu/Swathe - GitHub
文字通り、アンマネージ API という辛さを、.NET Framework のリフレクション API ライクな API で優しく包み、辛みを和らげてくれる"包帯"ですね。例えば「System.Linq.Enumerable クラスの Average メソッドについて、IEnumerable<int> を引数に取るオーバーロードのメタデータトークンは?」みたいな処理が、アンマネージ API を知らなくても、リフレクション API を知っていれば、こんな感じで、ごく自然に書けるようになってます(もちろん、C++ の初歩的な知識は必要なのですががが):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
・・・ | |
CPPANONYM_TEST(Urasandesu_Swathe_Test3, SampleForGetMethod_01) | |
{ | |
using namespace Urasandesu::Swathe::Hosting; | |
using namespace Urasandesu::Swathe::Metadata; | |
using namespace std; | |
// [ホスト API](http://msdn.microsoft.com/en-us/library/dd380850(v=vs.110).aspx) へ接続し、 | |
// [メタデータ API](http://msdn.microsoft.com/en-us/library/ms404430(v=vs.110).aspx) のラッパーを取得。 | |
auto const *pHost = HostInfo::CreateHost(); | |
auto const *pRuntime = pHost->GetRuntime(L"v4.0.30319"); | |
auto const *pMetaInfo = pRuntime->GetInfo<MetadataInfo>(); | |
auto *pMetaDisp = pMetaInfo->CreateDispenser(); | |
// mscorlib から、IEnumerable<int> を取得。 | |
auto const *pMSCorLib = pMetaDisp->GetAssembly(L"mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); | |
auto const *pMSCorLibDll = pMSCorLib->GetModule(L"CommonLanguageRuntimeLibrary"); | |
auto const *pIEnumerable1 = pMSCorLibDll->GetType(L"System.Collections.Generic.IEnumerable`1"); | |
auto const *pInt32 = pMSCorLibDll->GetType(L"System.Int32"); | |
auto const *pIEnumerable1Int32 = static_cast<IType *>(nullptr); | |
{ | |
auto genericArgs = vector<IType const *>(); | |
genericArgs.push_back(pInt32); | |
pIEnumerable1Int32 = pIEnumerable1->MakeGenericType(genericArgs); | |
} | |
// System.Core から Enumerable を取得。↑で取っておいた IEnumerable<int> を使って、Average メソッドを検索。 | |
auto const *pSystemCore = pMetaDisp->GetAssembly(L"System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); | |
auto const *pSystemCoreDll = pSystemCore->GetModule(L"System.Core.dll"); | |
auto const *pEnumerable = pSystemCoreDll->GetType(L"System.Linq.Enumerable"); | |
auto const *pEnumerable_Average_IEnumerable1Int32 = static_cast<IMethod *>(nullptr); | |
{ | |
auto params = vector<IType const *>(); | |
params.push_back(pIEnumerable1Int32); | |
pEnumerable_Average_IEnumerable1Int32 = pEnumerable->GetMethod(L"Average", params); | |
} | |
// 結果。 | |
ASSERT_TRUE(pEnumerable_Average_IEnumerable1Int32 != nullptr); | |
ASSERT_EQ(0x0600049B, pEnumerable_Average_IEnumerable1Int32->GetToken()); | |
} | |
・・・ |
Prig は、そのプロジェクトの説明に "lightweight framework" とありますが、これは冗談やネタではなく、単にメインのプロジェクトだけをクローンした場合、その規模は 7K loc ほどにしかならないという、本当にシンプルなライブラリになっています。ここまでシンプルにできているのは、この Swathe のおかげ。また、Prig は、その Language statistics を見ても、C# + PowerShell が 7 割ぐらいで、残りが C/C++、とほとんどがマネージコードになっていますので、興味を持っていただければ、雰囲気を掴むのはさほど難しくないでしょう。
えっ、サブモジュールを取り込むとどうなるの、ですか?・・・まだ要件の半分も満たしていないのに 98K loc ほどになります(白目)。こちら、中身が、PowerShell を使った自動生成コードによる依存関係管理やら、C++ の TMP を使ったオブジェクトファクトリー/リポジトリ自動生成やら、ネット上に情報の少ない各種アンマネージ API との格闘の跡やらで溢れていますので、まだしばらくは、人様に中身を紹介できるものにはならないでしょうね ...( = =)
閑話休題。ジェネリックタイプとジェネリックメソッドの迂廻路の雰囲気を紹介しましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Collections.Generic; | |
using UntestableLibrary; | |
namespace program1.MyLibrary | |
{ | |
public class Village | |
{ | |
・・・ | |
public Route GetShortestRoute(RicePaddy start, RicePaddy end) | |
{ | |
if (!m_shortestRoutesMap.ContainsKey(start)) | |
m_shortestRoutesMap[start] = CalculateShortestRoutes(start); | |
return m_shortestRoutesMap[start][end]; | |
} | |
// ダイクストラ法を用い、始田んぼから各田んぼへの最短経路を求めます。 | |
Dictionary<RicePaddy, Route> CalculateShortestRoutes(RicePaddy start) | |
{ | |
var shortestRoutes = new Dictionary<RicePaddy, Route>(); | |
var handled = new List<RicePaddy>(); | |
foreach (var ricePaddy in m_ricePaddies) | |
{ | |
shortestRoutes.Add(ricePaddy, new Route(ricePaddy.Identifier)); | |
} | |
shortestRoutes[start].TotalDistance = 0; | |
while (handled.Count != m_ricePaddies.Count) | |
{ | |
var shortestRicePaddies = shortestRoutes.OrderBy(_ => _.Value.TotalDistance).Select(_ => _.Key).ToArray(); | |
var processing = default(RicePaddy); | |
foreach (var ricePaddy in shortestRicePaddies) | |
{ | |
if (!handled.Contains(ricePaddy)) | |
{ | |
if (shortestRoutes[ricePaddy].TotalDistance == int.MaxValue) | |
return shortestRoutes; | |
processing = ricePaddy; | |
break; | |
} | |
} | |
var selectedRoads = m_roads.Where(_ => _.A == processing); | |
foreach (var road in selectedRoads) | |
{ | |
if (shortestRoutes[road.B].TotalDistance > road.Distance + shortestRoutes[road.A].TotalDistance) | |
{ | |
var roads = shortestRoutes[road.A].Roads.ToList(); | |
roads.Add(road); | |
shortestRoutes[road.B].Roads = roads; | |
shortestRoutes[road.B].TotalDistance = road.Distance + shortestRoutes[road.A].TotalDistance; | |
} | |
} | |
handled.Add(processing); | |
} | |
return shortestRoutes; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
・・・ | |
[Test] | |
public void GetShortestRoute_should_consider_routes_in_order_from_small_distance() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var slot = 0; | |
var numAndDistances = new[] { 4, 2, 4, 3, 1, 6, 7 }; | |
PRandom.Next_int.Body = (@this, maxValue) => numAndDistances[slot++]; | |
var vil = new Village(); | |
var considerations = new List<RicePaddy>(); | |
PList<RicePaddy>.Add.Body = (@this, item) => | |
{ | |
IndirectionsContext.ExecuteOriginal(() => | |
{ | |
considerations.Add(item); | |
@this.Add(item); | |
}); | |
}; | |
// Act | |
var result = vil.GetShortestRoute(vil.RicePaddies.ElementAt(2), vil.RicePaddies.ElementAt(0)); | |
// Assert | |
Assert.AreEqual(3, result.TotalDistance); | |
Assert.AreEqual(4, considerations.Count); | |
Assert.AreEqual(2, considerations[0].Identifier); | |
Assert.AreEqual(1, considerations[1].Identifier); | |
Assert.AreEqual(0, considerations[2].Identifier); | |
Assert.AreEqual(3, considerations[3].Identifier); | |
} | |
} | |
・・・ |
まずはジェネリックタイプのサンプルとして、先ほど取り上げた Village にまた登場してもらいました。このテストでは、指定した田んぼ間の最短経路を求める GetShortestRoute メソッドの内部状態を確認しています。Random.Next(int) を乗っ取っていますので、生成される田んぼと農道は、常に一定の数と距離で繋がれることになります。こんな感じですね:

GetShortestRoute メソッドはダイクストラ法を使って経路を求めていますので、ローカル変数 handled に「ある田んぼから行ける田んぼの内、まだ調べていない一番近い田んぼへの経路(を識別する田んぼ)」が、わかる度に追加されていきます。従って、ジェネリックタイプである List<T>.Add(T) を乗っ取って、そこに入ってくる要素が意図通りであれば、内部状態が確認できることになるでしょう。considerations に溜め込んだ要素を Assert しているのがこれに当たります。図の通り、
1. 【開始】田んぼ 2 → 0(距離: 4)、2 → 1(距離: 1)、2 → 3(距離: 7)を調べ、田んぼ 1 へ。
2. 田んぼ 1 → 0(距離: 2)、1 → 2(調査済み)、1 → 3(距離: 6)を調べ、田んぼ 0 へ。
3. 田んぼ 0 → 1(調査済み)、0 → 2(調査済み)、0 → 3(距離: 3)を調べ、田んぼ 3 へ。
4. 田んぼ 3 で、全ての経路が調べられたため完了。【終了】
と計算が進みますので、considerations に入ってるべきは、テストにある通り、田んぼ 2 → 田んぼ 1 → 田んぼ 0 → 田んぼ 3 となります。
次はジェネリックメソッド。AppDomain の解説で使った、懐かしのテストしにくい設定読み込みクラスに登場していただきます。今回は不運にも、それを使う側になったシチュエーションで ヽ(;▽;)ノ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using UntestableLibrary; | |
namespace program1.MyLibrary | |
{ | |
public static class LifeInfo | |
{ | |
・・・ | |
public static bool IsTodayHoliday() | |
{ | |
var dayOfWeek = DateTime.Today.DayOfWeek; | |
var holiday = ULConfigurationManager.GetProperty<DayOfWeek>("Holiday", DayOfWeek.Sunday); | |
switch (holiday) | |
{ | |
case DayOfWeek.Sunday: | |
return dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday; | |
case DayOfWeek.Monday: | |
return dayOfWeek == DayOfWeek.Sunday || dayOfWeek == DayOfWeek.Monday; | |
case DayOfWeek.Tuesday: | |
return dayOfWeek == DayOfWeek.Monday || dayOfWeek == DayOfWeek.Tuesday; | |
case DayOfWeek.Wednesday: | |
return dayOfWeek == DayOfWeek.Tuesday || dayOfWeek == DayOfWeek.Wednesday; | |
case DayOfWeek.Thursday: | |
return dayOfWeek == DayOfWeek.Wednesday || dayOfWeek == DayOfWeek.Thursday; | |
case DayOfWeek.Friday: | |
return dayOfWeek == DayOfWeek.Thursday || dayOfWeek == DayOfWeek.Friday; | |
case DayOfWeek.Saturday: | |
return dayOfWeek == DayOfWeek.Friday || dayOfWeek == DayOfWeek.Saturday; | |
default: | |
return dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday; | |
} | |
} | |
} | |
} |
あああ・・・。
「設定ファイルに設定された曜日とその前曜日は休日」という仕様なのでしょうが、switch 文と分岐で愚直に表現されています。サイクロマティック複雑度も 16 と、何とも香ばしい・・・。業務で出会った日には「上限値が決まった列挙型なのだから modulo で表現できたろうに・・・」「せめてテーブルで表現されてればまだ・・・リファクタリングしたい・・・」「あ、これ設定ファイルに依存してるからもしかして・・・やっぱりテストも無いああああくぁwせdrftgyふじこlp」「テストがなければ、みんな死ぬしk」と何かが濁っていく感じが味わえそう。
幸運なことに、Prig を紹介するにあたっては恰好のサンプルにしかなりません。「テストがなければ、仕様を囲うコードを書けば良いじゃない?」
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NUnit.Framework; | |
using program1.MyLibrary; | |
using System; | |
using System.Collections; | |
using System.Diagnostics; | |
using System.Prig; | |
using UntestableLibrary.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace Test.program1.MyLibraryTest | |
{ | |
[TestFixture] | |
public class LifeInfoTest | |
{ | |
・・・ | |
[Test] | |
[TestCaseSource(typeof(IsTodayHolidayTestSource), "TestCases")] | |
public bool IsTodayHoliday_should_consider_a_set_day_and_the_previous_day_as_holiday(DateTime today, DayOfWeek holiday) | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
PDateTime.TodayGet.Body = () => today; | |
PULConfigurationManager.GetProperty<DayOfWeek>.Body = (key, defaultValue) => holiday; | |
// Act, Assert | |
return LifeInfo.IsTodayHoliday(); | |
} | |
} | |
class IsTodayHolidayTestSource | |
{ | |
public static IEnumerable TestCases | |
{ | |
get | |
{ | |
yield return new TestCaseData(new DateTime(2013, 11, 16), DayOfWeek.Sunday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 17), DayOfWeek.Sunday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 18), DayOfWeek.Sunday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 17), DayOfWeek.Monday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 18), DayOfWeek.Monday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 19), DayOfWeek.Monday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 18), DayOfWeek.Tuesday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 19), DayOfWeek.Tuesday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 20), DayOfWeek.Tuesday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 19), DayOfWeek.Wednesday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 20), DayOfWeek.Wednesday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 21), DayOfWeek.Wednesday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 20), DayOfWeek.Thursday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 21), DayOfWeek.Thursday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 22), DayOfWeek.Thursday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 21), DayOfWeek.Friday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 22), DayOfWeek.Friday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 23), DayOfWeek.Friday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 22), DayOfWeek.Saturday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 23), DayOfWeek.Saturday).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 24), DayOfWeek.Saturday).Returns(false); | |
yield return new TestCaseData(new DateTime(2013, 11, 23), (DayOfWeek)99).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 24), (DayOfWeek)99).Returns(true); | |
yield return new TestCaseData(new DateTime(2013, 11, 25), (DayOfWeek)99).Returns(false); | |
} | |
} | |
} | |
} | |
} |
え、ちゃんと全部の分岐網羅できているか不安ですって?またそんな贅沢言ってこの娘は・・・しょうがないですね。
そんな方のために最後の機能、Profilers Chain を見ていきましょう!
Profilers Chain のサポート
Profilers Chain とは、複数のプロファイラを数珠繋ぎにして実行する機能です。なお、Profilers Chain という呼び名、私がこう呼んでいるだけで、本来の呼び方があるのかもしれません。詳しい方の突っ込み、お待ちしております <(_ _)>
直感的に「プロファイラを数珠繋ぎで実行?そんな風に使うことあるの??」と思われるかもしれませんが、.NET では、動的に IL を書き換えるタイミングが、プロファイル API が提供する JIT 時しかありません。ですので、実行する時に何かしらの計測や仕組みを入れ込む開発ツールは、必然的にここに集まることになります。メモリリークの検出や、パフォーマンス測定、トレースログ挿入によるデバッグ支援、埋め込みスクリプトによるエディット&コンティニュー…などなど。
問題になるのはこれらを組み合わせて使う時。プロファイル API の仕様上、一つのプロセスにアタッチできるプロファイラは自動的に一つになってしまいます。例えば、この Prig や Microsoft Fakes/Typemock Isolator/Telerik JustMock などのプロファイラベースの自動単体テスト向けテストダブル生成フレームワークと、NCover や OpenCover、JetBrains dotCover などのプロファイラベースのカバレッジ計測ツールを同時に使うと、「テストダブルは使えるがカバレッジが計測できない」「カバレッジは計測できるがテストダブルが使えない」と悲しみ溢れる状態に陥ります。

なので、本来プロファイラベースの某を作る場合は、その用途により、「もし既にプロファイラが組み込まれていたら、それを CLR からの指示に従って、透過的に実行してあげる」機能が必要かどうかを検討する必要があるのですね。

Prig は、Fakes の OSS 代替実装を謳う以上、この機能を持たないわけにはいかないので、もちろんサポートしています。OSS のカバレッジ計測ツールである OpenCover を使い、先ほどの例を実行してみましょう。引数が多くなってしまうのはどうしようもないのですが、こちらで紹介されている通り、*.bat を作っておくとちょっとは判りやすくなると思います:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"..\..\..\..\Release\x86\prig.exe" run -process "C:\Program Files (x86)\NUnit 2.6.3\bin\nunit-console-x86.exe" -arguments "Test.program1.dll /domain=None" |
で、実行します:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CMD x86>cd | |
C:\Prig\Test.program1\bin\Release(.NET 3.5)\x86 | |
CMD x86>"C:\Users\User\AppData\Local\Apps\OpenCover\OpenCover.Console.exe" -target:runtests.bat -filter:+[program1]* | |
Executing: C:\Prig\Test.program1\bin\Release(.NET 3.5)\x86\runtests.bat | |
CMD x86>"..\..\..\..\Release\x86\prig.exe" run -process "C:\Program Files (x86)\NUnit 2.6.3\bin\nunit-console-x86.exe" -arguments "Test.program1.dll /domain=None" | |
NUnit-Console version 2.6.3.13283 | |
Copyright (C) 2002-2012 Charlie Poole. | |
Copyright (C) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov. | |
Copyright (C) 2000-2002 Philip Craig. | |
All Rights Reserved. | |
Runtime Environment - | |
OS Version: Microsoft Windows NT 6.2.9200.0 | |
CLR Version: 2.0.50727.8000 ( Net 3.5 ) | |
ProcessModel: Default DomainUsage: None | |
Execution Runtime: net-3.5 | |
........................................................... | |
Tests run: 59, Errors: 0, Failures: 0, Inconclusive: 0, Time: 4.43196026064129 seconds | |
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0 | |
Committing... | |
Visited Classes 6 of 7 (85.71) | |
Visited Methods 12 of 18 (66.67) | |
Visited Points 83 of 106 (78.30) | |
Visited Branches 64 of 84 (76.19) | |
==== Alternative Results (includes all methods including those without corresponding source) ==== | |
Alternative Visited Classes 6 of 7 (85.71) | |
Alternative Visited Methods 26 of 34 (76.47) | |
CMD x86> |
ReportGenerator を使って結果を整形するとこんな感じ:


ちゃんと C0/C1 が網羅できていることが確認できました!もうリファクタリングに躊躇することはありません。保守しやすいコードに直し放題ですやったー! (((o(*゚▽゚*)o)))
ちなみにこの仕組み、Fakes が Visual Studio に標準搭載になって除外されてしまった残念な部分でもあります。Moles 時代はできたのに・・・自社製品で囲い込みたい気持ちは分かりますが、うーん (-_-;)
テスト!テスト!!テスト!!!
目に見える使い方は以上な感じ。あとは x86/x64 対応とか、.NET 4 の SxS 実行対応とか、地味な部分も手広くやってはいます。はじめはうんともすんとも言わなかった NUnit GUI での実行も、ここ数か月で動くようになり、安定性はだいぶ上がってきた実感がありますね。が、この解説記事向けのサンプルを書く中で未実装 OpCode が見つかるなど、やっぱりまだまだな状態。さあ、ここからが正念場です。テスト!テスト!!テスト!!!