プロジェクトページ:Prig Open Source Alternative to Microsoft Fakes By @urasandesu
NuGet:NuGet Gallery | Prig Open Source Alternative to Microsoft Fakes 1.0.0
テストやサンプルの追加が思ったよりうまく進み、また、課題だった部分が全て解決できたことや、ドキュメントがだいぶまとまってきたこともあり、予定より少し早めることができました。
リリースに際し、これまで日本語になっていなかったドキュメントは、この記事を含め順次日本語化していこうと思います。拙い英語なので、もし日本語記事を読んでいただいた後でドキュメントを読んで「あ、そういうことが言いたいんだったら、この言い回しにしたほうが良いよ」のようなことがありましたら、是非 @urasandesu にリプをしていただくなり、Issues に積んでいただくなりしていただければありがたいです。これから全 9 回を予定していますが、よろしければ最後までお付き合いくださいませ。
さて、導入手順であるクイックツアーがまた変わりましたので(だいぶ簡単になりましたよ!)、そこから解説していきましょう。加えて、今回は伝統的なモッキングフレームワークとの連携を。ちなみに Prig は、伝統的なモッキングフレームワークの機能をサポートしていません。OSS で既に必要な機能を持つライブラリが存在するのであれば、それらを使えば良いだけですからね。ただ、そのようなライブラリと連携できないことには話になりません。代表的なライブラリとして、Moq、NSubstitute、Rhino Mocks、FakeItEasy を例にとり、それらと Prig を連携して利用する方法を解説していきます。
それでは、行ってみましょう!
以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Getting Started with Unit Testing Part 3 | Visual Studio Toolbox | Channel 9
GenericParameterAttributes Enumeration (System.Reflection)
How can I fake OLEDB.OleDBDataAdapter to shim OleDBDataAdapter.Fill(dataSet) to shim results for unit testing using NUnit - Stack Overflow
c# - Can I Change Default Behavior For All Shims In A Test - Stack Overflow
IMetaDataImport2::GetVersionString Method
Obtaining List of Assemblies present in GAC
How should I create or upload a 32-bit and 64-bit NuGet package? - Stack Overflow
visual studio - Can NuGet distribute a COM dll? - Stack Overflow
Project Template in Visual Studio 2012 - CodeProject
Preferred way to mock/stub a system resource like System.Threading.Mutex - Stack Overflow
目次
クイックツアー(変更分)
NuGet への対応や、前回挙げていた懸念事項が全て解けたことを経て、導入手順はまたかなり変わりました。毎回の登場で恐縮ですが、「テストしにくい副作用を直接参照している処理」を例に見ていきます:
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 ConsoleApplication | |
{ | |
public static class LifeInfo | |
{ | |
public static bool IsNowLunchBreak() | |
{ | |
var now = DateTime.Now; | |
return 12 <= now.Hour && now.Hour < 13; | |
} | |
} | |
} |
手順としては以下のような感じ:
Step 1: NuGet からインストール
Step 2: スタブ設定の追加
Step 3: スタブ設定の修正
Step 4: テストの作成
Step 5: テストの実行
Final Step: リファクタリングしてキレイに!
「手順増えてるじゃないですかやだー」と思われるかもしれませんが、1 つ 1 つの手順が軽くなっているので大丈夫です (^-^; では、実際にやってみましょう!
Step 1: NuGet からインストール
Visual Studio 2013(Express for Windows Desktop 以上)を管理者権限で実行し、テストを追加します(例えば、ConsoleApplicationTest)。そして、以下のコマンドを Package Manager Console で実行します:
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
PM> Install-Package Prig |
※注:ほとんどの場合、インストールは上手く行きます。が、Visual Studio のインストール直後だと上手く行かないことがあるようです。こちらの Issue にあるコメントもご参照ください。
Step 2: スタブ設定の追加
以下のコマンドを Package Manager Console で実行します:
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
PM> Add-PrigAssembly -Assembly "mscorlib, Version=4.0.0.0" |
このコマンドは、テストのための間接スタブ設定を作成する、という意味です。mscorlib を指定しているのは、DateTime.Now が mscorlib に属しているからですね。コマンド実行後、外部からプロジェクトの変更があったけどどうします?という旨の確認メッセージが表示されますので、プロジェクトをリロードしてください。
Step 3: スタブ設定の修正
プロジェクトに設定ファイル <assembly name>.<runtime version>.v<assembly version>.prig を見つけられると思います(この場合だと、mscorlib.v4.0.30319.v4.0.0.0.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
<?xml version="1.0" encoding="utf-8"?> | |
<configuration> | |
<configSections> | |
<section name="prig" type="Urasandesu.Prig.Framework.PilotStubberConfiguration.PrigSection, Urasandesu.Prig.Framework" /> | |
</configSections> | |
<!-- | |
Get-IndirectionStubSetting コマンドで、add タブの内容を生成します。 | |
具体的には、Package Manager Console で、以下の PowerShell スクリプトにより、生成することができるでしょう: | |
========================== 例 1 ========================== | |
PM> $methods = Find-IndirectionTarget datetime get_Now | |
PM> $methods | |
Method | |
====== | |
System.DateTime get_Now() | |
PM> $methods[0] | Get-IndirectionStubSetting | clip | |
PM> | |
そうしたら、クリップボードの内容を、stubs タグの間に貼り付けます。 | |
--> | |
<prig> | |
<stubs> | |
<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, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</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> |
Step 4: テストの作成
テストコードにおいては、スタブの使用と偽の情報を返す Test Double への入れ替えを通じ、テスト可能になります:
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 ConsoleApplication; | |
using System; | |
using System.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace ConsoleApplicationTest | |
{ | |
[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 5: テストの実行
本来、プロファイラベースのモックツールを有効にするには、環境変数を弄る必要があります。そのため、そのようなライブラリ(Microsoft Fakes/Typemock Isolator/Telerik JustMock)は、要件を満たすための小さなランナーを提供しますが、それは Prig でも真となります。prig.exe を使い、以下の通りテストを実行します(引き続き Package Manager Console で)。
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
PM> cd <テストプロジェクトの出力ディレクトリ(例.cd .\ConsoleApplicationTest\bin\Debug)> | |
PM> prig run -process "C:\Program Files (x86)\NUnit 2.6.3\bin\nunit-console.exe" -arguments "ConsoleApplicationTest.dll /domain=None /framework=v4.0" | |
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: v4.0 | |
... | |
Tests run: 3, Errors: 0, Failures: 0, Inconclusive: 0, Time: 0.0934818542535837 seconds | |
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0 | |
PM> |
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 ConsoleApplication | |
{ | |
public static class LifeInfo | |
{ | |
public static bool IsNowLunchBreak() | |
{ | |
// 1. 外部環境から隔離するオーバーロードメソッドを追加し、元のメソッドからはそれを呼びます。 | |
return IsNowLunchBreak(DateTime.Now); | |
} | |
public static bool IsNowLunchBreak(DateTime now) | |
{ | |
// 2. さて、12 <= now.Hour && now.Hour < 13 は変に複雑でしたね。 | |
// このようが良さそうです。 | |
return now.Hour == 12; | |
} | |
// 3. リファクタリング後は、もはや Prig を使う必要はありません。このオーバーロードをテストすれば良いわけですから。 | |
} | |
} |
こんな感じで、Prig はテストしにくいライブラリに依存するようなコードをキレイにするのをサポートしてくれます。開発がまた楽しくなること請け合いですよ!
PowerShell モジュールによるサポートがだいぶ手厚くなったおかげで、手書きで *.csproj を触ったり、コマンドを組み立てたりする必要はなくなりました。ただ、その分環境に依存しやすくなっていますので、一長一短かなとも思います。あとはパフォーマンスですね。懸念事項が片付いた今なら、現状行っている DLL 解析⇒ソースコード生成⇒*.csproj 生成⇒MSBuild でソースコードをビルドし、スタブ Assembly を生成 という冗長な処理を、DLL 解析⇒スタブ Assembly を生成、にまで最適化できるはず。ただ、これには、アンマネージ API をラップしている Swathe を、だいぶ整理する必要があるため、次のメジャーバージョンアップ時を目指して、ぼちぼちやっていく予定です。他にも、Code Digger や、Pex のような、テストコード自動生成機能の搭載や、Telerik JustMock がやっているようなネイティブ API の入れ替え、導入手順のさらなる簡易化などもチャレンジしていきたいところですね。
既存 Mocking ライブラリとの連携
次は既存 Mocking ライブラリとの連携です。以下のクラスのテストについて考えてみましょう:
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.ComponentModel; | |
using System.Runtime.CompilerServices; | |
namespace TraditionalMockingFrameworkSample | |
{ | |
public class NotifyingObject : INotifyPropertyChanged | |
{ | |
public event PropertyChangedEventHandler PropertyChanged; | |
int m_valueWithRandomAdded; | |
public int ValueWithRandomAdded | |
{ | |
get { return m_valueWithRandomAdded; } | |
set | |
{ | |
m_valueWithRandomAdded = value; | |
m_valueWithRandomAdded += new Random((int)DateTime.Now.Ticks).Next(); | |
OnPropertyChanged(); | |
} | |
} | |
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") | |
{ | |
var handler = PropertyChanged; | |
if (handler == null) | |
return; | |
handler(this, new PropertyChangedEventArgs(propertyName)); | |
} | |
} | |
} |
たぶん、「ValueWithRandomAdded は、PropertyChanged イベントをその名前で発火するべき」や「ValueWithRandomAdded は、渡された値 + Random.Next() を保持するべき」のようなテストをしたくなると思います。後者について、Moq、NSubstitute、Rhino Mocks、FakeItEasy を使ってテストをする連携サンプルを紹介します。
共通の前準備として、Random.Next() のための間接スタブ設定を追加します。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
PM> Add-PrigAssembly -Assembly "mscorlib, Version=4.0.0.0" |
スタブ設定ファイルが追加されたら、クラス Random のためのスタブ設定をクリップボードにコピーするため、以下のコマンドを実行します:
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
PM> [System.Random] | Find-IndirectionTarget | Get-IndirectionStubSetting | Clip |
そして、それをスタブ設定ファイル mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます。それでは、それぞれのモッキングフレームワークとの連携を見ていきましょう!
Moq
Moq の連携サンプルです。
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 Moq; | |
using NUnit.Framework; | |
using System; | |
using System.ComponentModel; | |
using System.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace TraditionalMockingFrameworkSample | |
{ | |
[TestFixture] | |
public class MoqTest | |
{ | |
[Test] | |
public void ValueWithRandomAdded_should_hold_passed_value_plus_random_value() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var notifyingObject = new NotifyingObject(); | |
var mockNext = new Mock<IndirectionFunc<Random, int>>(); | |
mockNext.Setup(_ => _(It.IsAny<Random>())).Returns(10); | |
PRandom.Next().Body = mockNext.Object; | |
// Act | |
notifyingObject.ValueWithRandomAdded = 32; | |
var actual = notifyingObject.ValueWithRandomAdded; | |
// Assert | |
mockNext.Verify(_ => _(It.IsAny<Random>()), Times.Once()); | |
Assert.AreEqual(42, actual); | |
} | |
} | |
} | |
} |
NSubstitute
NSubstitute の連携サンプルです。
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 NSubstitute; | |
using NUnit.Framework; | |
using System; | |
using System.ComponentModel; | |
using System.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace TraditionalMockingFrameworkSample | |
{ | |
[TestFixture] | |
public class NSubstituteTest | |
{ | |
[Test] | |
public void ValueWithRandomAdded_should_hold_passed_value_plus_random_value() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var notifyingObject = new NotifyingObject(); | |
var mockNext = Substitute.For<IndirectionFunc<Random, int>>(); | |
mockNext(Arg.Any<Random>()).Returns(10); | |
PRandom.Next().Body = mockNext; | |
// Act | |
notifyingObject.ValueWithRandomAdded = 32; | |
var actual = notifyingObject.ValueWithRandomAdded; | |
// Assert | |
mockNext.Received(1)(Arg.Any<Random>()); | |
Assert.AreEqual(42, actual); | |
} | |
} | |
} | |
} |
Rhino Mocks
Rhino Mocks の連携サンプルです。
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 Rhino.Mocks; | |
using System; | |
using System.ComponentModel; | |
using System.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace TraditionalMockingFrameworkSample | |
{ | |
[TestFixture] | |
public class RhinoMocksTest | |
{ | |
[Test] | |
public void ValueWithRandomAdded_should_hold_passed_value_plus_random_value() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var notifyingObject = new NotifyingObject(); | |
var mockNext = MockRepository.GenerateStub<IndirectionFunc<Random, int>>(); | |
mockNext.Stub(_ => _(Arg<Random>.Is.Anything)).Return(10); | |
PRandom.Next().Body = mockNext; | |
// Act | |
notifyingObject.ValueWithRandomAdded = 32; | |
var actual = notifyingObject.ValueWithRandomAdded; | |
// Assert | |
mockNext.AssertWasCalled(_ => _(Arg<Random>.Is.Anything), options => options.Repeat.Once()); | |
Assert.AreEqual(42, actual); | |
} | |
} | |
} | |
} |
FakeItEasy
FakeItEasy の連携サンプルです。
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 FakeItEasy; | |
using NUnit.Framework; | |
using System; | |
using System.ComponentModel; | |
using System.Prig; | |
using Urasandesu.Prig.Framework; | |
namespace TraditionalMockingFrameworkSample | |
{ | |
[TestFixture] | |
public class FakeItEasyTest | |
{ | |
[Test] | |
public void ValueWithRandomAdded_should_hold_passed_value_plus_random_value() | |
{ | |
using (new IndirectionsContext()) | |
{ | |
// Arrange | |
var notifyingObject = new NotifyingObject(); | |
var mockNext = A.Fake<IndirectionFunc<Random, int>>(); | |
A.CallTo(() => mockNext(A<Random>._)).Returns(10); | |
PRandom.Next().Body = mockNext; | |
// Act | |
notifyingObject.ValueWithRandomAdded = 32; | |
var actual = notifyingObject.ValueWithRandomAdded; | |
// Assert | |
A.CallTo(() => mockNext(A<Random>._)).MustHaveHappened(); | |
Assert.AreEqual(42, actual); | |
} | |
} | |
} | |
} |
え?「全部同じようなサンプルに見える」ですって?はい、その通りです。重要なことはただ一つ、「デリゲートのためのモックを作成することを意識する」だけですから。そのような機能持っているものの中から、お好きなものをお選びください!(>ω・)
終わりに
つい先日、マイクロソフトから .NET Framework のオープンソース化や、フル機能無料版の Visual Studio の提供、.NET Server Framework の Linux/MacOS X 向けディストリビューションの展開の発表があり、大きなニュースになりましたね。
ただ、オープンソース化された .NET Framework コアを見ると、本当のコア部分(ランタイムホストや JIT、プロファイラ内でも動くような制限のないメタデータ操作、リソースの検索・・・etc。いわゆる、昔、SSCLI として公開された範囲ですね)は含まれていないですし(.NET Core Runtime について、"We’re currently figuring out the plan for open sourcing the runtime. Stay tuned!"と言っているので何かしら提供する気はありそうなのですが・・・)、無償化された Visual Studio Community 2013 はビジネスユースが限定されていたり、その機能は Professional 止まりだったりと、押さえるところは押さえてるっていう印象です。まあ、経営戦略上、必要なものを公開・無償化したというだけと言えばだけなのかもしれません。
私がずっと追っている自動ユニットテスト関連の Visual Studio 拡張の中でも、Microsoft Fakes は、Community エディションには搭載されませんでした。Fakes は、当初から全エディションに搭載して!という声が上がっているにも関わらず、Premium 以上の機能のままですので、こちらも当たり前と言えば当たり前。また、その前身の Moles が動く Visual Studio 2010 のメインストリームサポートが、来年の夏ごろ終わることを考えると、このタイミングで無償化されなかったということは、今後無償化されるとしても良いタイミングになるとは言えない気がします。
そんなこんなで、私が 5 年前から作っていたこのライブラリも、Microsoft Fakes のオープンソース代替実装として、収まるところに収まってしまいました。いや正直、開発し始めた当時、こんな大仰なことを謳えるようになるとは思ってもみなかったですががが… (∩´﹏`∩) 。
まだまだ至らない点があるかと思いますが、これを機に、Prig、.NET 開発のお供として、末永くお付き合いいただければ幸いでございます。
0 件のコメント:
コメントを投稿