2014年11月17日月曜日

クイックツアー変更分と既存 Mocking ライブラリとの連携 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

リリースです!!

  プロジェクトページ: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 で既に必要な機能を持つライブラリが存在するのであれば、それらを使えば良いだけですからね。ただ、そのようなライブラリと連携できないことには話になりません。代表的なライブラリとして、MoqNSubstituteRhino MocksFakeItEasy を例にとり、それらと 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 への対応や、前回挙げていた懸念事項が全て解けたことを経て、導入手順はまたかなり変わりました。毎回の登場で恐縮ですが、「テストしにくい副作用を直接参照している処理」を例に見ていきます:
using System;
namespace ConsoleApplication
{
public static class LifeInfo
{
public static bool IsNowLunchBreak()
{
var now = DateTime.Now;
return 12 <= now.Hour && now.Hour < 13;
}
}
}
view raw 01_01.cs hosted with ❤ by GitHub

手順としては以下のような感じ:
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 で実行します:
PM> Install-Package Prig
view raw 01_02.ps1 hosted with ❤ by GitHub

※注:ほとんどの場合、インストールは上手く行きます。が、Visual Studio のインストール直後だと上手く行かないことがあるようです。こちらの Issue にあるコメントもご参照ください


Step 2: スタブ設定の追加
以下のコマンドを Package Manager Console で実行します:
PM> Add-PrigAssembly -Assembly "mscorlib, Version=4.0.0.0"
view raw 01_03.ps1 hosted with ❤ by GitHub

このコマンドは、テストのための間接スタブ設定を作成する、という意味です。mscorlib を指定しているのは、DateTime.Now が mscorlib に属しているからですね。コマンド実行後、外部からプロジェクトの変更があったけどどうします?という旨の確認メッセージが表示されますので、プロジェクトをリロードしてください。


Step 3: スタブ設定の修正
プロジェクトに設定ファイル <assembly name>.<runtime version>.v<assembly version>.prig を見つけられると思います(この場合だと、mscorlib.v4.0.30319.v4.0.0.0.prig ですね)。設定ファイルをコメントに従い修正したら、全てのプロジェクトをビルドします:
<?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>
view raw 01_04.xml hosted with ❤ by GitHub



Step 4: テストの作成
テストコードにおいては、スタブの使用と偽の情報を返す Test Double への入れ替えを通じ、テスト可能になります:
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);
}
}
}
}
view raw 01_05.cs hosted with ❤ by GitHub



Step 5: テストの実行
本来、プロファイラベースのモックツールを有効にするには、環境変数を弄る必要があります。そのため、そのようなライブラリ(Microsoft Fakes/Typemock Isolator/Telerik JustMock)は、要件を満たすための小さなランナーを提供しますが、それは Prig でも真となります。prig.exe を使い、以下の通りテストを実行します(引き続き Package Manager Console で)。

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>
view raw 01_06.ps1 hosted with ❤ by GitHub


Final Step: リファクタリングしてキレイに!
テストができてしまえば、リファクタリングに躊躇はなくなりますね!例えば、以下のようにリファクタリングできるでしょう:
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 を使う必要はありません。このオーバーロードをテストすれば良いわけですから。
}
}
view raw 01_07.cs hosted with ❤ by GitHub

こんな感じで、Prig はテストしにくいライブラリに依存するようなコードをキレイにするのをサポートしてくれます。開発がまた楽しくなること請け合いですよ!


PowerShell モジュールによるサポートがだいぶ手厚くなったおかげで、手書きで *.csproj を触ったり、コマンドを組み立てたりする必要はなくなりました。ただ、その分環境に依存しやすくなっていますので、一長一短かなとも思います。あとはパフォーマンスですね。懸念事項が片付いた今なら、現状行っている DLL 解析⇒ソースコード生成⇒*.csproj 生成⇒MSBuild でソースコードをビルドし、スタブ Assembly を生成 という冗長な処理を、DLL 解析⇒スタブ Assembly を生成、にまで最適化できるはず。ただ、これには、アンマネージ API をラップしている Swathe を、だいぶ整理する必要があるため、次のメジャーバージョンアップ時を目指して、ぼちぼちやっていく予定です。他にも、Code Digger や、Pex のような、テストコード自動生成機能の搭載や、Telerik JustMock がやっているようなネイティブ API の入れ替え、導入手順のさらなる簡易化などもチャレンジしていきたいところですね。





既存 Mocking ライブラリとの連携
次は既存 Mocking ライブラリとの連携です。以下のクラスのテストについて考えてみましょう:
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));
}
}
}
view raw 01_08.cs hosted with ❤ by GitHub

たぶん、「ValueWithRandomAdded は、PropertyChanged イベントをその名前で発火するべき」や「ValueWithRandomAdded は、渡された値 + Random.Next() を保持するべき」のようなテストをしたくなると思います。後者について、MoqNSubstituteRhino MocksFakeItEasy を使ってテストをする連携サンプルを紹介します。

共通の前準備として、Random.Next() のための間接スタブ設定を追加します。Prig をインストールし、各テストプロジェクトで以下のコマンドを実行してください:
PM> Add-PrigAssembly -Assembly "mscorlib, Version=4.0.0.0"
view raw 01_13.ps1 hosted with ❤ by GitHub

スタブ設定ファイルが追加されたら、クラス Random のためのスタブ設定をクリップボードにコピーするため、以下のコマンドを実行します:
PM> [System.Random] | Find-IndirectionTarget | Get-IndirectionStubSetting | Clip
view raw 01_14.ps1 hosted with ❤ by GitHub

そして、それをスタブ設定ファイル mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます。それでは、それぞれのモッキングフレームワークとの連携を見ていきましょう!



Moq
Moq の連携サンプルです。
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);
}
}
}
}
view raw 01_09.cs hosted with ❤ by GitHub



NSubstitute
NSubstitute の連携サンプルです。
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);
}
}
}
}
view raw 01_10.cs hosted with ❤ by GitHub



Rhino Mocks
Rhino Mocks の連携サンプルです。
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);
}
}
}
}
view raw 01_11.cs hosted with ❤ by GitHub



FakeItEasy
FakeItEasy の連携サンプルです。
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);
}
}
}
}
view raw 01_12.cs hosted with ❤ by GitHub

え?「全部同じようなサンプルに見える」ですって?はい、その通りです。重要なことはただ一つ、「デリゲートのためのモックを作成することを意識する」だけですから。そのような機能持っているものの中から、お好きなものをお選びください!(>ω・)




終わりに
つい先日、マイクロソフトから .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 件のコメント:

コメントを投稿