2014年12月8日月曜日

非公開メソッドの入れ替え - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

ソフトウェアテストあどべんとかれんだー2014 8日目!&まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 9 段!(元ネタ:Prigwiki より、FEATURES: Non Public Method Replacement。同シリーズの他記事:1 2 3 4 5 6 7 8

はじめましての方ははじめまして!ソフトウェアテストあどべんとかれんだー2014 8日目を担当させていただきます、@urasandesu こと杉浦と申します。

前日は、@hayabusa333 さんの、Ruby - Gauntltによるセキュリティテスト #SWTestAdvent - Qiita でしたね。セキュリティテスト自動化フレームワーク、そんなものもあるのだと興味深く拝読させていただきました。2014 年は、Heartbleed や、Apache Struts の脆弱性ShellshockPOODLE と、脆弱性の話題に事欠かない年になってしまいましたが、今後セキュリティに関するテストはますます重要になっていくんでしょうね。

さて、ソフトウェアテストに関連することということで、私からは打って変わって実装寄りのお話を。以前から私が作成しています Prig という、.NET 自動ユニットテスト向け迂廻路生成ライブラリについて、紹介させていただこうと思います。迂廻路生成?と思われるかもしれませんが、簡単に言うと、通常は行えない static メソッドや private メソッドの上書きをできるようにするというものです(.NET だと Microsoft Fakes、Java だと JMockit とかが有名どころでしょうか)。

題材は「既存ライブラリの非公開メソッドが絡む自動ユニットテスト」。言語は C# です。よく言われる通り、非公開なメソッドそのものをテストすることはよくないこととされていますが、テストで非公開なメソッドに対して何かしたくなることは、しばしばあるんじゃないでしょうか?例えば、テスト対象のメソッド内で使われる private な setter を持つプロパティに意味のある値を与えておきたいWeb にアクセスしにいってしまう private メソッドをモックに入れ替えたい、などなど。まあ、今まさに開発中のコンポーネントであれば、いくらでも対処方法はあるのですが、すでに稼働しているシステムだったり、外部から買い入れたコンポーネントだったりすると、途端に難易度が跳ね上がるのが困りもの。

ところで、C# の特徴的な機能の 1 つに、今から 7 年ほど前に出た C# 3.0 で追加された、拡張メソッドという機能があります。皆さんは拡張メソッドは好きですか?乱用するべきではないですが、その機能が、本質的に、そのライブラリがそのレイヤーでサポートしてほしいものであれば、設計上自然な API を実現できることがあるかと、私は思います。ただし、そのライブラリが、そのような拡張に対してオープンであるかどうかは、場合によるでしょう。特に、そのシグネチャに、非公開な属性の 1 つである internal なクラスが現れるようなメソッドが関係する場合は要注意。今回は、Prig によって、どのようにこの問題を解決するかを解説したいと思います。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
How to mock ConfigurationManager.AppSettings with moq - Stack Overflow
TDD, Unit testing and Microsoft Fakes with Sitecore Solutions
Basic mocking techniques - Stack Overflow
Hybrid Framework - http://our.umbraco.org
Unit testing Umbraco 7 | just this guy
How to Write 3v1L, Untestable Code
Generic Methods Implementation in Microsoft Fakes - CodeProject
Paulo Morgado - Mastering Expression Trees With .NET Reflector
Expression Tree Visualizer for VS 2010 - Home
mocking - Using Microsoft Fakes Framework with VSTO Application-Level Add-in an XML based Ribbon - Stack Overflow





目次

非公開メソッドの入れ替え
既存ライブラリに、以下のような DTO 群があるとしましょう:
class ULTableStatus
{
internal bool IsOpened = false;
internal int RowsCount = 0;
}
public class ULColumn
{
public ULColumn(string name)
{
Name = name;
}
public string Name { get; private set; }
}
public class ULColumns : IEnumerable
{
ULTableStatus m_status;
List<ULColumn> m_columns = new List<ULColumn>();
internal ULColumns(ULTableStatus status)
{
m_status = status;
}
public void Add(ULColumn column)
{
ValidateState(m_status);
m_columns.Add(column);
}
public void Remove(ULColumn column)
{
ValidateState(m_status);
m_columns.Remove(column);
}
public IEnumerator GetEnumerator()
{
return m_columns.GetEnumerator();
}
static void ValidateState(ULTableStatus status)
{
if (!status.IsOpened)
throw new InvalidOperationException("The column can not be modified because owner table has not been opened.");
if (0 < status.RowsCount)
throw new ArgumentException("The column can not be modified because some rows already exist.");
}
}
public class ULTable
{
ULTableStatus m_status = new ULTableStatus();
public ULTable(string tableName)
{
TableName = tableName;
Columns = new ULColumns(m_status);
}
public string TableName { get; private set; }
public ULColumns Columns { get; private set; }
public void Open(string connectionString)
{
// ここで DB に接続し、このクラスへスキーマ情報を設定する。
...(snip)...
// 「準備完了」を表すフラグを立てる。
m_status.IsOpened = true;
}
}
view raw 09_01.cs hosted with ❤ by GitHub

「DB への接続」という副作用と、「テーブル自体のデータ」という状態を 1 つのクラスで管理しており、嫌な臭いを感じます。ただ、このライブラリを作ったニンゲンが、これ以上のリファクタリングをするモチベーションを持つことはないかもしれません。なぜならば、このライブラリだけ見れば internal なクラスが緩衝材としてあり、InternalsVisibleToAttribute を使えば、制限なくそのクラスにアクセスができるため、テストをするのに特に問題を感じないでしょうから。

さて、このライブラリはテーブルスキーマの自動生成ツールも提供しており、特定の列を自動生成してくれます。そのような列は、以下のような規約で命名されるとのことです:
  • <table name> + _ID ・・・ プライマリキー
  • DELETED ・・・ 論理削除フラグ
  • CREATED ・・・ 作成日時
  • MODIFIED ・・・ 更新日時
なるほどなるほど。そうすると「自動生成された列だけを取得する」や「手動で生成された列だけを取得する」などのようなことがやりたくなりますね。残念ながら、既存のライブラリは、そのような機能を提供していないとのこと。なので、今回は自分で作成することにしました。こんなシチュエーションでは拡張メソッドがピッタリでしょう。テストを書いてみます:
[TestFixture]
public class ULTableMixinTest
{
[Test]
public void GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
{
// Arrange
var expected = new[] { new ULColumn("USER_ID"), new ULColumn("DELETED"), new ULColumn("CREATED"), new ULColumn("MODIFIED") };
var users = new ULTable("USER");
users.Columns.Add(expected[0]);
users.Columns.Add(new ULColumn("PASSWORD"));
users.Columns.Add(new ULColumn("USER_NAME"));
users.Columns.Add(expected[1]);
users.Columns.Add(expected[2]);
users.Columns.Add(expected[3]);
// Act
var actual = users.GetAutoGeneratedColumns();
// Assert
CollectionAssert.AreEqual(expected, actual);
}
}
view raw 09_02.cs hosted with ❤ by GitHub

テーブル USER には、列 USER_ID、PASSWORD、USER_NAME、DELETED、CREATED、MODIFIED があるとします。そのテーブルに対し、拡張メソッド GetAutoGeneratedColumns を実行すると、自動生成された列が取得できるという寸法です。このままではビルドすら通りませんので、とりあえず以下のような最低限の実装を用意しました:
public static class ULTableMixin
{
public static IEnumerable<ULColumn> GetAutoGeneratedColumns(this ULTable @this)
{
throw new NotImplementedException();
}
}
view raw 09_03.cs hosted with ❤ by GitHub

ほい、実行っと。NotImplementedException がスローされるでしょうから、とりあえずなコードを追記し・・・て・・・あれ?
PM> & "C:\Program Files (x86)\NUnit 2.6.3\bin\nunit-console.exe" MyLibraryTest.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.8009 ( Net 3.5 )
ProcessModel: Default DomainUsage: None
Execution Runtime: v4.0
.F
Tests run: 1, Errors: 1, Failures: 0, Inconclusive: 0, Time: 0.284553726293366 seconds
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0
Errors and Failures:
1) Test Error : MyLibraryTest.Mixins.UntestableLibrary.ULTableMixinTest.GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated
System.InvalidOperationException : The column can not be modified because owner table has not been opened.
at UntestableLibrary.ULColumns.ValidateState(ULTableStatus status)
at UntestableLibrary.ULColumns.Add(ULColumn column)
at MyLibraryTest.Mixins.UntestableLibrary.ULTableMixinTest.GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
PM>
view raw 09_04.ps1 hosted with ❤ by GitHub

ヴァー!変なとこで引っかかっとる!! ('A`)

実は、スタックトレースにも出力されているように、ULColumns は、メソッド ValidateState を使うことによって、列が変更可能かどうかを検証しています。テストケースにおいては、GetAutoGeneratedColumns を検証したいのですが、その前、users.Columns.Add(expected[0]); で例外がスローされていたわけですね。これはいけません・・・。

このような状況で、Prig を使うことで、不必要な検証を一時的に外すことができます。Prig をインストールし、その Assembly のスタブ設定を追加します:
PM> Add-PrigAssembly -AssemblyFrom <ULColumns の Assembly へのフルパス。例えば、"C:\Users\User\NonPublicUntestable\UntestableLibrary\bin\Debug\UntestableLibrary.dll">
view raw 09_05.ps1 hosted with ❤ by GitHub

以下のコマンドを実行し、ULColumns の設定をクリップボードにコピーします:
PM> Add-Type -Path <ULColumns の Assembly へのフルパス。例えば、"C:\Users\User\NonPublicUntestable\UntestableLibrary\bin\Debug\UntestableLibrary.dll">
PM> Find-IndirectionTarget ([<ULColumns のフルネーム。例えば、UntestableLibrary.ULColumns>]) ValidateState | Get-IndirectionStubSetting | Clip
view raw 09_06.ps1 hosted with ❤ by GitHub

そうしましたら、追加されたスタブ設定ファイル(例:UntestableLibrary.v4.0.30319.v1.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>
<prig>
<stubs>
<add name="ValidateStateULTableStatus" alias="ValidateStateULTableStatus">
<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="">ValidateState</Name>
<AssemblyName z:Id="3" z:Type="System.String" z:Assembly="0" xmlns="">UntestableLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</AssemblyName>
<ClassName z:Id="4" z:Type="System.String" z:Assembly="0" xmlns="">UntestableLibrary.ULColumns</ClassName>
<Signature z:Id="5" z:Type="System.String" z:Assembly="0" xmlns="">Void ValidateState(UntestableLibrary.ULTableStatus)</Signature>
<Signature2 z:Id="6" z:Type="System.String" z:Assembly="0" xmlns="">System.Void ValidateState(UntestableLibrary.ULTableStatus)</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 09_07.ps1 hosted with ❤ by GitHub

上の準備が終わったら、以下のようにテストを書き直すことができるようになります:
[Test]
public void GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
{
using (new IndirectionsContext())
{
// Arrange
// Prig を使い、副作用に依存する検証を抑制するため、メソッド本体を入れ替えます。
PULColumns.ValidateStateULTableStatus().Body = args => null;
var expected = new[] { new ULColumn("USER_ID"), new ULColumn("DELETED"), new ULColumn("CREATED"), new ULColumn("MODIFIED") };
var users = new ULTable("USER");
users.Columns.Add(expected[0]);
users.Columns.Add(new ULColumn("PASSWORD"));
users.Columns.Add(new ULColumn("USER_NAME"));
users.Columns.Add(expected[1]);
users.Columns.Add(expected[2]);
users.Columns.Add(expected[3]);
// Act
var actual = users.GetAutoGeneratedColumns();
// Assert
CollectionAssert.AreEqual(expected, actual);
}
}
view raw 09_08.cs hosted with ❤ by GitHub

こんどはどうでしょう?
PM> prig run -process "C:\Program Files (x86)\NUnit 2.6.3\bin\nunit-console.exe" -arguments "MyLibraryTest.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.8009 ( Net 3.5 )
ProcessModel: Default DomainUsage: None
Execution Runtime: v4.0
.F
Tests run: 1, Errors: 1, Failures: 0, Inconclusive: 0, Time: 1.07656140775011 seconds
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0
Errors and Failures:
1) Test Error : MyLibraryTest.Mixins.UntestableLibrary.ULTableMixinTest.GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated
System.NotImplementedException : The method or operation is not implemented.
at MyLibrary.Mixins.UntestableLibrary.ULTableMixin.GetAutoGeneratedColumns(ULTable this)
at MyLibraryTest.Mixins.UntestableLibrary.ULTableMixinTest.GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
PM>
view raw 09_09.ps1 hosted with ❤ by GitHub

よし!今度は NotImplementedException がスローされるという、意図した結果になりました。後は、テストを通すコードを書き、全てのテストが通ったらリファクタリングをし、新たなテストを追加する、という黄金の回転を回すだけ。良い感じじゃないですか?





付録
ところで、かの人が元のライブラリをどのように設計していれば、もっと簡単にテストができていたと思いますか?そもそも、状態を副作用から分離したライブラリとして再設計すべきだとは思いますが、既存のライブラリで、インターフェイスを変更するような再設計は難しいでしょうね。せめてなにかできることがあるとすれば、非 public なインターフェイスを public にするような変更ぐらいでしょう。

なお、次の点には注意してください:単にインターフェイスを公開する、例えば、ULTableStatus の全てのフィールドと、ULColumns のコンストラクタ .ctor(ULTableStatus) を公開するようなことをしてしまえば、簡単にデータの不整合が起きるようになってしまい、ライブラリが安全ではなくなってしまいます。ライブラリの安全性が保たれる範囲のインターフェイスだけを公開するべきでしょう。このケースでは、以下のような変更が考えられます:
...(snip)...
public class ULColumns : IEnumerable
{
List<ULColumn> m_columns = new List<ULColumn>();
IValidation<ULColumns> m_val;
public ULColumns(IValidation<ULColumns> val)
{
if (val == null)
throw new ArgumentNullException("val");
m_val = val;
}
public void Add(ULColumn column)
{
m_val.Validate(this);
m_columns.Add(column);
}
public void Remove(ULColumn column)
{
m_val.Validate(this);
m_columns.Remove(column);
}
...(snip)...
internal static IValidation<ULColumns> GetDefaultValidation(ULTableStatus status)
{
return new ColumnsVariabilityValidator(status);
}
class ColumnsVariabilityValidator : IValidation<ULColumns>
{
readonly ULTableStatus m_status;
public ColumnsVariabilityValidator(ULTableStatus status)
{
m_status = status;
}
public void Validate(ULColumns t)
{
if (!m_status.IsOpened)
throw new InvalidOperationException("The column can not be modified because owner table has not been opened.");
if (0 < m_status.RowsCount)
throw new ArgumentException("The column can not be modified because some rows already exist.");
}
}
}
public class ULTable
{
ULTableStatus m_status = new ULTableStatus();
public ULTable(string tableName)
{
TableName = tableName;
}
...(snip)...
ULColumns m_columns;
public virtual ULColumns Columns
{
get
{
if (m_columns == null)
m_columns = new ULColumns(ULColumns.GetDefaultValidation(m_status));
return m_columns;
}
}
...(snip)...
}
public interface IValidation<T>
{
void Validate(T obj);
}
view raw 09_10.cs hosted with ❤ by GitHub

ULColumns のコンストラクタは公開しましたが、ULTableStatus を直接指定することはせず、検証のためのメソッドだけを持ったインターフェイス IValidation を代わりに指定するようにしました。Prig を使って入れ替えたかったメソッド ValidateState が持つ機能を、外出ししたことになります。対象のメソッドは状態にアクセスはしますが、それを書き換えることはしません。従って、その部分を公開するだけであれば、ライブラリのデータの整合性は保ち続けられることになります。それから、ULTable の ULColumns を生成するプロパティを virtual 化します。

これらの再設計により、Prig を使わなければキーとなるメソッドに到達することすら難しかったテストは、以下のようにできます。Moq のような通常のモックフレームワークで、簡単にテストができるようになるのです:
[Test]
public void GetAutoGeneratedColumns_should_return_columns_that_are_auto_generated()
{
// Arrange
// Moq を使い、副作用に依存する検証を抑制するため、メソッド本体を入れ替えます。
var usersMock = new Mock<ULTable>("USER");
usersMock.Setup(_ => _.Columns).Returns(new ULColumns(new Mock<IValidation<ULColumns>>().Object));
var expected = new[] { new ULColumn("USER_ID"), new ULColumn("DELETED"), new ULColumn("CREATED"), new ULColumn("MODIFIED") };
var users = usersMock.Object;
users.Columns.Add(expected[0]);
users.Columns.Add(new ULColumn("PASSWORD"));
users.Columns.Add(new ULColumn("USER_NAME"));
users.Columns.Add(expected[1]);
users.Columns.Add(expected[2]);
users.Columns.Add(expected[3]);
// Act
var actual = users.GetAutoGeneratedColumns();
// Assert
CollectionAssert.AreEqual(expected, actual);
}
view raw 09_11.cs hosted with ❤ by GitHub





終わりに
ソフトウェアテストあどべんとかれんだー2014 8日目、C# と自作ライブラリを題材に、自動ユニットテストにおける非公開メソッドの入れ替えを解説してみました。つい先月、V1.0.0 をリリースしたばかりということもあり、まだまだ問題もあるかと思いますが、もし興味を持っていただき、使っていただければ嬉しいです!問題などあれば、是非 @urasandesu 宛てにお気軽に mention 下さいませ (((o(*゚▽゚*)o)))

さて、私のまとまってきたドキュメントを日本語の記事にもしておくよシリーズ(1 2 3 4 5 6 7 8)はこれで終わりですが、ソフトウェアテストあどべんとかれんだー2014 はまだまだ続きますのでお見逃しなく!

明日は、@PoohSunny さん。よろしくどうぞ!!

2014年12月6日土曜日

移行サンプル:Typemock Isolator による MessageBox を使うテストのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 8 段!(元ネタ:Prigwiki より、MIGRATION: Test using MessageBox by Typemock Isolator。同シリーズの他記事:1 2 3 4 5 6 7

このシリーズも残り僅か。今回もよろしくお願いします。Typemock は、Isolator の Quick Start で、MessageBox をモックに入れ替えるサンプルを紹介しています。これも Prig(と Moq)に移行することが可能です。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Testing code that rely on Microsoft Azure Management Libraries using Microsoft Fakes
c# - The type is defined in an assembly that is not referenced, how to find the cause - Stack Overflow
.net - Mircosoft fakes - shims without ShimsContext - Stack Overflow
Unit Test for ShimDataTableCollection Count
c# - How to know if a MemberInfo is an explicit implementation of a property - Stack Overflow
#5816 (any_range requires copyable elements) – Boost C++ Libraries
#10360 (Since 1.56, any_range use static cast of reference instead of implicit conversion) – Boost C++ Libraries
#10493 (Since 1.56, any_range with non-reference references can cause UB) – Boost C++ Libraries
hunting bugs with git bisect and submodules - Least Significant Bit
AdventCalendar - git bisect で問題箇所を特定する - Qiita
便利!電動歯ブラシ | Boost.勉強会 #16 大阪





目次

準備
まずは、間接設定を作成する必要があります。Package Manager Console を開き、Default project: をテストプロジェクトに変更してください。その後、以下のコマンドを実行します:
PM> dir
Directory: C:\users\akira\documents\visual studio 2013\Projects\IsolatorMigrationDemo
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2014/10/10 9:31 IsolatorMigrationDemo
d---- 2014/10/10 9:37 IsolatorMigrationDemoTest
d---- 2014/10/10 9:37 packages
-a--- 2014/10/10 9:32 1561 IsolatorMigrationDemo.sln
PM> cd .\IsolatorMigrationDemo\bin\Debug
PM> dir
Directory: C:\users\akira\documents\visual studio 2013\Projects\IsolatorMigrationDemo\IsolatorMigrationDemo\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/10/10 9:31 7680 IsolatorMigrationDemo.exe
-a--- 2014/10/10 9:30 189 IsolatorMigrationDemo.exe.config
-a--- 2014/10/10 9:31 28160 IsolatorMigrationDemo.pdb
-a--- 2014/10/10 9:30 23168 IsolatorMigrationDemo.vshost.exe
-a--- 2014/10/10 9:30 189 IsolatorMigrationDemo.vshost.exe.config
-a--- 2013/06/18 21:28 490 IsolatorMigrationDemo.vshost.exe.manifest
PM> padd -af (dir .\IsolatorMigrationDemo.exe).FullName
PM> padd -as "System.Windows.Forms, Version=4.0.0.0"
PM>
view raw 08_01.cs hosted with ❤ by GitHub

次に、Isolator のサンプルで使用しているメソッドのための間接設定を取得しましょう。PowerShell(コンソール)を開き、情報を取得するために以下のコマンドを実行します:
PS> $pwd
Path
----
C:\Users\Akira\Documents\Visual Studio 2013\Projects\IsolatorMigrationDemo\IsolatorMigrationDemo\bin\Debug
PS> powershell
Windows PowerShell
Copyright (C) 2013 Microsoft Corporation. All rights reserved.
PS> ipmo "C:\Users\Akira\Documents\Visual Studio 2013\Projects\IsolatorMigrationDemo\packages\Prig.1.0.0\tools\Urasandesu.Prig"
PS> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\IsolatorMigrationDemo\IsolatorMigrationDemo\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/10/10 9:31 7680 IsolatorMigrationDemo.exe
-a--- 2014/10/10 9:30 189 IsolatorMigrationDemo.exe.config
-a--- 2014/10/10 9:31 28160 IsolatorMigrationDemo.pdb
-a--- 2014/10/10 9:30 23168 IsolatorMigrationDemo.vshost.exe
-a--- 2014/10/10 9:30 189 IsolatorMigrationDemo.vshost.exe.config
-a--- 2013/06/18 21:28 490 IsolatorMigrationDemo.vshost.exe.manifest
PS> $asmInfo = [System.Reflection.Assembly]::LoadFrom((dir .\IsolatorMigrationDemo.exe).FullName)
PS> $asmInfo.GetTypes()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False Form1 System.Windows.Forms.Form
False False Program System.Object
True False SomeClass System.Object
True False UserOfSomeClass System.Object
False False Resources System.Object
False False Settings System.Configuration.ApplicationSettingsBase
PS> $asmInfo.GetTypes() | ? { $_.Name -match 'Class' }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False SomeClass System.Object
True False UserOfSomeClass System.Object
PS> $asmInfo.GetTypes() | ? { $_.Name -match 'Class' } | pfind
Method
------
Void MyMethod()
Void .ctor()
Void DoSomething()
Void .ctor()
PS> $asmInfo.GetTypes() | ? { $_.Name -match 'Class' } | pfind | pget | clip # この結果については、IsolatorMigrationDemo.v4.0.30319.v1.0.0.0.prig に貼り付けてください。
PS> $asmInfo = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
PS> $asmInfo.GetTypes() | ? { $_.Name -eq 'messagebox' }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False MessageBox System.Object
PS> $asmInfo.GetTypes() | ? { $_.Name -eq 'messagebox' } | pfind -m 'show\(system\.string\)'
Method
------
System.Windows.Forms.DialogResult Show(System.String)
PS> $asmInfo.GetTypes() | ? { $_.Name -eq 'messagebox' } | pfind -m 'show\(system\.string\)' | pget | clip # この結果については、System.Windows.Forms.v4.0.30319.v4.0.0.0.prig に貼り付けてください。
PS> exit
PS>
view raw 08_02.ps1 hosted with ❤ by GitHub

Visual Studio に戻り、IsolatorMigrationDemo.v4.0.30319.v1.0.0.0.prig と System.Windows.Forms.v4.0.30319.v4.0.0.0.prig に各々の間接設定を貼り付けます。ビルドが成功したら、サンプルを移行していきますよ!





Example Test 1 - Simple test using MessageBox
Isolator は、プロファイリング API による強力なメソッドの入れ替え機能に加え、JustMock と同様、Mock Object を生成する機能を持っています。Prig はそのような機能をサポートしていませんが、最初に説明した通りMoq と連携することで、それを実現することができましたね。
[Test]
public void MessageBoxShow_should_be_callable_indirectly()
{
using (new IndirectionsContext())
{
// Arrange
var mockMessageBox = new Mock<IndirectionFunc<string, DialogResult>>();
mockMessageBox.Setup(_ => _(string.Empty)).Returns(DialogResult.OK);
PMessageBox.ShowString().Body = mockMessageBox.Object;
// Act
MessageBox.Show("This is a message");
// Assert
mockMessageBox.Verify(_ => _("This is a message"));
}
}
view raw 08_03.cs hosted with ❤ by GitHub

Isolate.WhenCalled は、Prig の間接スタブ(この場合、PMessageBox.ShowString().Body)に、Moq.Mock.Setup でセットアップした Mock Object を割り当てることで、置き換えることができます。Isolate.Verify.WasCalledWithExactArguments は、Moq.Mock.Verify と機能的に同じですね。問題は無いでしょう。次へ行きますよ!





Example Test 2 - Complex Test
「複雑な」と付いていますが、そう難しいものではありません ( ̄ー ̄)
[Test]
public void UserOfSomeClassDoSomething_should_show_MessageBox_if_an_exception_is_thrown()
{
using (new IndirectionsContext())
{
// Arrange
PSomeClass.MyMethod().Body = () => { throw new Exception("foo"); };
var mockMessageBox = new Mock<IndirectionFunc<string, DialogResult>>();
mockMessageBox.Setup(_ => _(string.Empty)).Returns(DialogResult.OK);
PMessageBox.ShowString().Body = mockMessageBox.Object;
// Act
var user = new UserOfSomeClass();
user.DoSomething();
// Assert
mockMessageBox.Verify(_ => _("Exception caught: foo"));
}
}
view raw 08_04.cs hosted with ❤ by GitHub

特別な条件が無いのであれば、Isolate.WhenCalled(..).WillThrow は、Prig の間接スタブ(この場合、PSomeClass.MyMethod().Body)に、直接例外をスローする関数を割り当てることで、置き換えることができます。Isolate.WhenCalled(..).WillReturn や Isolate.Verify.WasCalledWithExactArguments は、前に説明しましたので・・・おっと、これで全部です!

ちなみに、対象が、MessageBox 処理があるにも関わらずテストコードを書きたくなるほど複雑な場合、設計をしくじっている可能性が高いと、個人的には思います。既存のコードやレガシーコードに対しては仕方がないでしょうが、こんなライブラリのような闇の力を、新規のプロダクトコードには使わなくて済むことを願いたいものですね (^^ゞ



2014年12月4日木曜日

移行サンプル:Telerik JustMock によるモック化③ 静的メソッドのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 7 段!(元ネタ:Prigwiki より、MIGRATION: Static Mocking by Telerik JustMock。同シリーズの他記事:1 2 3 4 5 6

3 回に渡る JustMock の公式ドキュメントにあるサンプルを実装するシリーズ、Part 3 となります。最後は、「Static Mocking samples」を Prig に移行してみましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
How often are fakes assemblies generated? - Stack Overflow
Brownfield Development: Taming Legacy Code with Better Unit Testing and Microsoft Fakes
Nested Types in Generic Classes - Haibo Luo's weblog - MSDN Blogs
c# - Behavedbase in fakes - Stack Overflow
CLR Profiler - Documentation
Resize image in the wiki of github usin markdown - Stack Overflow
Behind iPhone's Critical Security Bug, a Single Bad 'Goto' WIRED
microsoft fakes only stub static property of a static class - Stack Overflow
Home Page - Code Impact - .NET Community Event
PowerShell で SIGPIPE 連鎖 - NyaRuRuが地球にいたころ
Generics and Your Profiler - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs





目次

準備
このサンプルでは、シリーズということもあり、1 つのソリューションに複数のプロジェクトを追加するという構成を採っています(前回前々回の記事もご参照くださいませ)。これまでのサンプルと同様、Package Manager Console と PowerShell を使って説明を続けたいと思います。各使用コマンドは以前のサンプルで解説していますので、詳細はそちらもご覧ください。

さて、間接スタブ設定を作成しましょう。Package Manager Console を開き、Default project: をテストプロジェクトに変更します
PM> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2014/09/17 19:34 FinalMockingMigration
d---- 2014/09/23 16:55 FinalMockingMigrationTest
d---- 2014/09/28 8:46 packages
d---- 2014/09/27 6:45 SealedMockingMigration
d---- 2014/09/27 7:06 SealedMockingMigrationTest
d---- 2014/09/16 20:37 StaticMockingMigration
d---- 2014/09/21 11:35 StaticMockingMigrationTest
-a--- 2014/09/16 6:31 3665 JustMockMigrationDemo.sln
PM> cd .\StaticMockingMigration\bin\Debug
PM> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\StaticMockingMigration\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/09/25 6:18 5120 StaticMockingMigration.exe
-a--- 2014/09/16 6:31 189 StaticMockingMigration.exe.config
-a--- 2014/09/25 6:18 15872 StaticMockingMigration.pdb
PM> padd -af (dir .\StaticMockingMigration.exe).FullName
PM> padd -as "System.Web, Version=4.0.0.0"
PM>
view raw 07_01.ps1 hosted with ❤ by GitHub

次に、Assembly を解析します。PowerShell(コンソール)を開き、以下のコマンドで情報を取得します:
PS> $pwd
Path
----
C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\StaticMockingMigration\bin\Debug
PS> powershell
Windows PowerShell
Copyright (C) 2013 Microsoft Corporation. All rights reserved.
PS> ipmo "C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\packages\Prig.1.0.0\tools\Urasandesu.Prig"
PS> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\StaticMockingMigration\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/09/28 11:51 5632 StaticMockingMigration.exe
-a--- 2014/09/16 6:31 189 StaticMockingMigration.exe.config
-a--- 2014/09/28 11:51 17920 StaticMockingMigration.pdb
PS> $asmInfo = [System.Reflection.Assembly]::LoadFrom((dir .\StaticMockingMigration.exe).FullName)
PS> $asmInfo.GetTypes()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False Foo System.Object
False False FooInternal System.Object
True False FooStatic System.Object
True False Bar System.Object
True False BarExtensions System.Object
False False Program System.Object
PS> $asmInfo.GetTypes() | pfind
Method
------
Void Submit()
Int32 Execute(Int32)
Int32 get_FooProp()
Void set_FooProp(Int32)
Void .cctor()
Void .ctor()
Void DoIt()
Void .ctor()
Void Do()
Void Execute()
Void .ctor()
Int32 Echo(StaticMockingMigration.Bar, Int32)
Void Main(System.String[])
Void .ctor()
PS> $asmInfo.GetTypes() | pfind | pget | clip # この結果については、StaticMockingMigration.v4.0.30319.v1.0.0.0.prig に貼り付けてください。
PS> $asmInfo = [System.Reflection.Assembly]::LoadWithPartialName("System.Web")
PS> $asmInfo.GetTypes() | ? { $_.Name -eq 'httpcontext' }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False HttpContext System.Object
PS> $asmInfo.GetTypes() | ? { $_.Name -eq 'httpcontext' } | pfind -m 'get_current\b'
Method
------
System.Web.HttpContext get_Current()
PS> $asmInfo.GetTypes() | ? { $_.Name -eq 'httpcontext' } | pfind -m 'get_current\b' | pget | clip # この結果については、System.Web.v4.0.30319.v4.0.0.0.prig に貼り付けてください。
PS> exit
PS>
view raw 07_02.ps1 hosted with ❤ by GitHub

Visual Studio に戻り、各間接設定 StaticMockingMigration.v4.0.30319.v1.0.0.0.prig、System.Web.v4.0.30319.v4.0.0.0.prig に貼り付けます。ビルドは通りましたか?さあ、実際に移行してみましょう!





Static Constructor Mocking
静的コンストラクタの間接スタブは、文字通り StaticConstructor という名前になります:
[Test]
public void Prig_should_arrange_static_function()
{
using (new IndirectionsContext())
{
// Arrange
PFoo.StaticConstructor().Body = () => { };
PFoo.FooPropGet().Body = () => 0;
// Act
var actual = Foo.FooProp;
// Assert
Assert.AreEqual(0, actual);
}
}
view raw 07_03.cs hosted with ❤ by GitHub






General Static Method Mocking
一般的な静的メソッドのモック化です。んー、これは Moq と連携したほうがわかりやすいでしょうね:
[Test]
public void Prig_should_throw_when_not_arranged()
{
using (new IndirectionsContext())
{
// Arrange
PFoo.StaticConstructor().Body = () => { };
var executeMock = new Mock<IndirectionFunc<int, int>>(MockBehavior.Strict);
executeMock.Setup(_ => _(10)).Returns(10);
PFoo.ExecuteInt32().Body = executeMock.Object;
var submitMock = new Mock<IndirectionAction>(MockBehavior.Strict);
PFoo.Submit().Body = submitMock.Object;
// Act, Assert
Assert.AreEqual(10, Foo.Execute(10));
Assert.Throws<MockException>(() => Foo.Submit());
}
}
view raw 07_04.cs hosted with ❤ by GitHub

ところで、個人的には ExpectedException より Assert.Throws のほうが好きだったり。ExpectedException を使うケースだと、意図しない場所で例外が発生しても(例えば、上記の例だと、Foo.Execute(10) が例外をスロー時)、テストは成功してしまいますからね。





Mocking Static Property Get
静的プロパティの getter を入れ替えます。何度か説明してきているように思いますが・・・一応紹介しておきましょう (^_^;)
[Test]
public void Prig_should_fake_static_property_get()
{
using (new IndirectionsContext())
{
// Arrange
PFoo.StaticConstructor().Body = () => { };
var called = false;
PFoo.FooPropGet().Body = () => { called = true; return 1; };
// Act
var actual = Foo.FooProp;
// Assert
Assert.AreEqual(1, actual);
Assert.IsTrue(called);
}
}
view raw 07_05.cs hosted with ❤ by GitHub






Mocking Static Property Set
静的プロパティの setter を入れ替えます:
[Test]
public void Prig_should_fake_static_property_set()
{
using (new IndirectionsContext())
{
// Arrange
PFoo.StaticConstructor().Body = () => { };
var fooPropSetMock = new Mock<IndirectionAction<int>>(MockBehavior.Strict);
fooPropSetMock.Setup(_ => _(10));
PFoo.FooPropSetInt32().Body = fooPropSetMock.Object;
// Act, Assert
Foo.FooProp = 10;
}
}
view raw 07_06.cs hosted with ❤ by GitHub

検証の方法について、JustMock のサンプルとは若干違いがあります。個人的には、MockBehavior.Strict を指定すれば、条件によってメソッドが呼び出されているかどうかを再度検証する必要はないとは思いますね。条件を満たさないメソッドの呼び出しがあれば、自動的に例外がスローされますので。





Mocking Internal Static Call
internal な静的メソッドを入れ替える例です。コメントに残すだけでなく、例外がスローされないことも検証すべきでしょう:
[Test]
public void Prig_should_fake_internal_static_call()
{
using (new IndirectionsContext())
{
// Arrange
PFooInternal.DoIt().Body = () => { };
// Act, Assert
Assert.DoesNotThrow(() => FooInternal.DoIt());
}
}
view raw 07_07.cs hosted with ❤ by GitHub






Mocking Static Class
静的クラスのメソッドを入れ替えるサンプルです。説明すべきことはあまりないですね (^-^;
[Test]
public void Prig_should_mock_static_class()
{
using (new IndirectionsContext())
{
// Arrange
PFooStatic.Do().Body = () => { };
// Act, Assert
Assert.DoesNotThrow(() => FooStatic.Do());
}
}
view raw 07_08.cs hosted with ❤ by GitHub






Mocking Current HttpContext
現在の HTTP コンテキストを入れ替えるサンプルです。本来であれば、元の処理は HTTP リクエスト中にのみ有効なのですが、もはや制限はありません。
[Test]
public void Prig_should_assert_mocking_http_context()
{
using (new IndirectionsContext())
{
// Arrange
var called = false;
PHttpContext.CurrentGet().Body = () => { called = true; return null; };
// Act
var ret = HttpContext.Current;
// Assert
Assert.IsTrue(called);
}
}
view raw 07_09.cs hosted with ❤ by GitHub






Mocking Extension Methods
拡張メソッドの入れ替えです。結局のところ、拡張メソッドは静的メソッドですので、前までのサンプルと同じように入れ替えることができます:
[Test]
public void Prig_should_fake_extension_method()
{
using (new IndirectionsContext())
{
// Arrange
var foo = new Bar();
var echoMock = new Mock<IndirectionFunc<Bar, int, int>>();
echoMock.Setup(_ => _(foo, 10)).Returns(11);
PBarExtensions.EchoBarInt32().Body = echoMock.Object;
// Act
var actual = foo.Echo(10);
// Assert
Assert.AreEqual(11, actual);
}
}
view raw 07_10.cs hosted with ❤ by GitHub




2014年12月1日月曜日

移行サンプル:Telerik JustMock によるモック化② シールされたクラスのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 6 段!(元ネタ:Prigwiki より、MIGRATION: Sealed Mocking by Telerik JustMock。同シリーズの他記事:1 2 3 4 5

3 回に渡る JustMock の公式ドキュメントにあるサンプルを実装するシリーズ、Part 2 となります。今回は、「Sealed Mocking samples」を Prig に移行してみましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Unity を使って AOP - present
How do microsoft fakes' shims actually work internally? - Stack Overflow
c# - Good and free unit-testing alternatives to Telerik's JustMock - Software Recommendations Stack Exchange
Unable to create Fakes for Google APIs - Stack Overflow
Covering basics of unit testing with Typemock - .NET Unit Testing Tips
The Difference Between Unit Tests and Integration Tests - Typemock Isolator
Typemock Isolator Quick Start -
Home - Run Jekyll on Windows
Setup Jekyll on Windows - Yi Zeng
OctopressをWindows7にインストールしてみたメモ by @pon_zu on @Qiita





目次

準備
このサンプルでは、シリーズということもあり、1 つのソリューションに複数のプロジェクトを追加するという構成を採っています。前回のサンプルと同様、Package Manager Console と PowerShell を使って説明を続けたいと思います。各使用コマンドは前回のサンプルで解説していますので、詳細はそちらもご覧ください。

まずは、間接スタブ設定を作成しましょう:
PM> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2014/09/17 19:34 FinalMockingMigration
d---- 2014/09/23 16:55 FinalMockingMigrationTest
d---- 2014/09/25 6:13 packages
d---- 2014/09/16 20:32 SealedMockingMigration
d---- 2014/09/16 20:34 SealedMockingMigrationTest
d---- 2014/09/16 20:37 StaticMockingMigration
d---- 2014/09/21 11:35 StaticMockingMigrationTest
-a--- 2014/09/16 6:31 3665 JustMockMigrationDemo.sln
PM> cd .\SealedMockingMigration\bin\Debug
PM> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\SealedMockingMigration\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/09/25 6:18 5120 SealedMockingMigration.exe
-a--- 2014/09/16 6:31 189 SealedMockingMigration.exe.config
-a--- 2014/09/25 6:18 13824 SealedMockingMigration.pdb
PM> padd -af (dir .\SealedMockingMigration.exe).FullName
PM>
view raw 06_01.ps1 hosted with ❤ by GitHub

あー・・・「やっべ!Default project: にテストプロジェクト設定してねえ!変なとこに設定が追加された!!!!」って方。

お気の毒さまですが、Prig はそれを削除するコマンドをサポートしていません(PreBuildEvent 内の解析ェ・・・)。恐縮ですが、手動で *.csproj を元に戻す必要があります。以下のイメージのように、\.prig" にマッチするタグ Reference、None、PreBuildEvent の全てを削除してください:









Git のようなバージョン管理システム下で作業することを強くお勧めしておきます (^q^)

次は、PowerShell(コンソール)による対象メソッドの絞り込みです。慣れれば一気にできるでしょう:
PS> $pwd
Path
----
C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\SealedMockingMigration\bin\Debug
PS> powershell
Windows PowerShell
Copyright (C) 2013 Microsoft Corporation. All rights reserved.
PS> ipmo "C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\packages\Prig.1.0.0\tools\Urasandesu.Prig"
PS> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\SealedMockingMigration\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/09/27 6:45 5120 SealedMockingMigration.exe
-a--- 2014/09/16 6:31 189 SealedMockingMigration.exe.config
-a--- 2014/09/27 6:45 17920 SealedMockingMigration.pdb
PS> $asmInfo = [System.Reflection.Assembly]::LoadFrom((dir .\SealedMockingMigration.exe).FullName)
PS> $asmInfo.GetTypes()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False FooSealed System.Object
True False FooSealedInternal System.Object
True False IFoo
True False Foo System.Object
False False Program System.Object
PS> $asmInfo.GetTypes() | pfind
Method
------
Int32 Echo(Int32)
Void .ctor()
Int32 Echo(Int32)
Void .ctor()
Void Execute()
Void SealedMockingMigration.IFoo.Execute(Int32)
Void .ctor()
Void Main(System.String[])
Void .ctor()
PS> $asmInfo.GetTypes() | pfind | pget | clip
PS> exit
PS>
view raw 06_02.ps1 hosted with ❤ by GitHub

今回は、Moles の例以降、表に出てこなかったコマンド pfind(Find-IndirectionTarget)を使っています。実はこのコマンド、前回までのサンプルで、標準リフレクション API によって掛けていたざっくりとしたフィルタを、既に含んでいます。なので、上記のように手順の簡易化ができるようになるんですね。そうしたら、Visual Studio に戻り、前回と同様 SealedMockingMigration.v4.0.30319.v1.0.0.0.prig に貼り付けます。ビルドは通りましたか?さあ、実際の移行をしてみましょう!





Assert Final Method Call on a Sealed Class
シールされたクラスの final メソッドを置き換えてみましょう:
[Test]
public void Prig_should_assert_final_method_call_on_a_sealed_class()
{
using (new IndirectionsContext())
{
// Arrange
PFooSealed.EchoInt32().Body = (@this, arg1) => 10;
// Act
var actual = new FooSealed().Echo(1);
// Assert
Assert.AreEqual(10, actual);
}
}
view raw 06_03.cs hosted with ❤ by GitHub

特定のインスタンスだけの置き換えるのでない時は、PProxy<original class name> を使うより、P<original class name> を使うほうが簡単かも。





Create Mock for Sealed Class with Internal Constructor
ところで、入れ替える型のコンストラクタが非 public だとどうなるのか、心配になったりします?:
[Test]
public void Prig_should_create_mock_for_a_sealed_class_with_internal_constructor()
{
using (new IndirectionsContext())
{
// Arrange
var fooProxy = new PProxyFooSealedInternal();
fooProxy.EchoInt32().Body = (@this, arg1) => 10;
var foo = (FooSealedInternal)fooProxy;
// Act
var actual = foo.Echo(1);
// Assert
Assert.AreEqual(10, actual);
}
}
view raw 06_04.cs hosted with ❤ by GitHub

はい!何も問題ありませんね!





Create Mock for Sealed Class with Interface
これはインターフェイスを使った振る舞いの例です。まずは直呼び出し:
[Test]
public void Prig_should_assert_call_on_void()
{
using (new IndirectionsContext())
{
// Arrange
var called = false;
PFoo.Execute().Body = @this => called = true;
// Act
new Foo().Execute();
// Assert
Assert.IsTrue(called);
}
}
view raw 06_05.cs hosted with ❤ by GitHub

次に、インターフェイスを通じた呼び出し:
[Test]
public void Prig_should_assert_call_on_void_through_an_interface()
{
using (new IndirectionsContext())
{
// Arrange
var called = false;
PFoo.Execute().Body = @this => called = true;
var foo = new Foo();
// Act
var iFoo = (IFoo)foo;
iFoo.Execute();
// Assert
Assert.IsTrue(called);
}
}
view raw 06_06.cs hosted with ❤ by GitHub

加えて、明示的に実装されたインターフェイスのケースです。これについては、JustMock はサンプルを準備していないようですね。なので、一応念のため説明させていただきます:
[Test]
public void Prig_should_assert_call_on_void_through_an_explict_implemented_interface()
{
using (new IndirectionsContext())
{
// Arrange
var called = false;
PFoo.SealedMockingMigrationIFooExecuteInt32().Body = (@this, arg1) => called = true;
var foo = new Foo();
// Act
var iFoo = (IFoo)foo;
iFoo.Execute(1);
// Assert
Assert.IsTrue(called);
}
}
view raw 06_07.cs hosted with ❤ by GitHub

明示的に実装されたインターフェースを入れ替える時は、間接スタブが特殊な名前になることだけに気を付ければ大丈夫です。通常の名前は、単に <メソッドの間接スタブ名> ですが、この場合の名前は、<名前空間> + <インターフェイスの間接スタブ名> + <メソッドの間接スタブ名> となります。



2014年11月26日水曜日

移行サンプル:Telerik JustMock によるモック化① final メソッドのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 5 段!(元ネタ:Prigwiki より、MIGRATION: Final Mocking by Telerik JustMock。同シリーズの他記事:1 2 3 4

やっと折り返し地点ですね。今回からは 3 回に渡る JustMock の公式ドキュメントにあるサンプルを実装するシリーズとなります。まずは、「Final Mocking samples」を Prig に移行してみましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Using Jekyll with Pages - GitHub Help
Can I share a Microsoft Fakes unit test with a developer using Visual Studio Professional - Stack Overflow
shim a sealed class singleton method and with MS Fakes - Stack Overflow
Search - GitHub Anonymously Hosted DynamicMethods Assembly
API Hooking with MS Detours - CodeProject
Incorrect solution build ordering when using MSBuild.exe - The Visual Studio Blog - Site Home - MSDN Blogs
visual studio 2010 - How do I target a specific .NET project within a Solution using MSBuild from VS2010 - Stack Overflow
Advanced Usage | JustMock Documentation
.net - Is there any free mocking framework that can mock static methods and sealed classes - Stack Overflow
Modern ASP.NET Webskills by @calebjenkins





目次

準備
このサンプルでは、シリーズということもあり、1 つのソリューションに複数のプロジェクトを追加するという構成を採っています。





なお、この例は、間接的に呼び出される対象が、GAC に登録されていない Assembly に含まれており、以前の例(MolesFakes)とは異なることに注意してくださいね。

これまでは Package Manager Console で間接スタブを作成する手順を実施しても問題ありませんでしたが、今後は、PowerShell(コンソール)を使えるようになることを推奨します。Package Manager Console は、仕様上、ネストしたプロンプトをサポートしていません。従って、1 度間接対象の解析を行うために Assembly を読み込むと、2 度とその Assembly を解放できないという問題があるのです。そもそも、いつも使う機能である、オートコンプリート、コマンド履歴なども PowerShell(コンソール)のものより機能的に劣ります。間接設定の追加やテストの実行時は Package Manager Console を使い、Assembly の解析時は PowerShell(コンソール)を使うということが、効率が良いでしょう。

※注※:以下の解説で、PM> で始まるコマンドは Package Manager Console で実行しますが、PS> で始まるコマンドは PowerShell(コンソール)で実行することに注意してください。

FinalMockingMigration をビルド後、出力ディレクトリを開き、以下のコマンドを実行します。プロジェクトがいくつもありますので、対象のテストプロジェクトを Default project: として選択することをお忘れなく:
PM> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2014/09/16 6:37 FinalMockingMigration
d---- 2014/09/16 20:30 FinalMockingMigrationTest
d---- 2014/09/16 6:35 packages
d---- 2014/09/16 20:32 SealedMockingMigration
d---- 2014/09/16 20:34 SealedMockingMigrationTest
d---- 2014/09/16 20:37 StaticMockingMigration
d---- 2014/09/16 20:40 StaticMockingMigrationTest
-a--- 2014/09/16 6:31 3665 JustMockMigrationDemo.sln
PM> cd .\FinalMockingMigration\bin\Debug
PM> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\FinalMockingMigration\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/09/16 6:38 6144 FinalMockingMigration.exe
-a--- 2014/09/16 6:30 189 FinalMockingMigration.exe.config
-a--- 2014/09/16 6:38 13824 FinalMockingMigration.pdb
-a--- 2014/09/17 6:31 23168 FinalMockingMigration.vshost.exe
-a--- 2014/09/16 6:30 189 FinalMockingMigration.vshost.exe.config
-a--- 2013/06/18 21:28 490 FinalMockingMigration.vshost.exe.manifest
PM> padd -af (dir .\FinalMockingMigration.exe).FullName
PM>
view raw 05_01.ps1 hosted with ❤ by GitHub

ちなみに、padd -af は、Add-PrigAssembly -AssemblyFrom のエイリアスです。GAC に登録されていない Assembly の間接スタブ設定を追加する場合、コマンドの引数にフルパスを引き渡す必要があることに注意してください。PowerShell(スクリプト言語)では、コマンド (dir <target file>).FullName でフルファイルパスを取得することができます。FinalMockingMigrationTest に FinalMockingMigration.v4.0.30319.v1.0.0.0.prig が追加されれば成功です。

さて、次は、FinalMockingMigration の出力ディレクトリ上で PowerShell(コンソール)を実行し、Assembly を解析しましょう。Windows 8 を使っているのであれば、エクスプローラで対象のディレクトリを表示中、Alt、F、R のキーコンビネーションが便利ですよね。現在の位置を確認したら、ネストしたプロンプトを開始します:
PS> $pwd
Path
----
C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\FinalMockingMigration\bin\Debug
PS> powershell
Windows PowerShell
Copyright (C) 2013 Microsoft Corporation. All rights reserved.
PS>
view raw 05_02.ps1 hosted with ❤ by GitHub

Package Manager Console で利用が可能になっている種々のコマンド(Find-IndirectionTarget や Get-IndirectionStubSetting など)は、Prig をインストール後、モジュール $(SolutionDir)\packages\Prig.\tools\Urasandesu.Prig に配置されます。なので、それを PowerShell(コンソール)にインポートします:
PS> ipmo "C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\packages\Prig.1.0.0\tools\Urasandesu.Prig"
PS> gmo
ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Manifest 3.1.0.0 Microsoft.PowerShell.Management {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Con...
Manifest 3.1.0.0 Microsoft.PowerShell.Utility {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Script 0.0.0.0 Urasandesu.Prig {Add-PrigAssembly, ConvertTo-PrigAssemblyName, Find-Indire...
PS>
view raw 05_03.ps1 hosted with ❤ by GitHub

GAC に登録していない Assembly を読み込みたい場合、System.Reflection.Assembly.LoadFrom(string) にフルパスを引き渡す必要があります。取得には・・・って、ちょっと前にやりましたね (^^ゞ:
PS> dir
Directory: C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\FinalMockingMigration\bin\Debug
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2014/09/16 6:38 6144 FinalMockingMigration.exe
-a--- 2014/09/16 6:30 189 FinalMockingMigration.exe.config
-a--- 2014/09/16 6:38 13824 FinalMockingMigration.pdb
-a--- 2014/09/17 6:31 23168 FinalMockingMigration.vshost.exe
-a--- 2014/09/16 6:30 189 FinalMockingMigration.vshost.exe.config
-a--- 2013/06/18 21:28 490 FinalMockingMigration.vshost.exe.manifest
PS> (dir .\FinalMockingMigration.exe).FullName
C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\FinalMockingMigration\bin\Debug\FinalMockingMigration.exe
view raw 05_04.ps1 hosted with ❤ by GitHub

Assembly を読み込み後、変数に設定します。ところで、履歴から実行したコマンドを呼び出し、ちょっと変更して再実行するという流れは PowerShell(コンソール)の真骨頂だと思います。Package Manager Console で同じことをしようものなら、重複した履歴を延々と遡る必要があったり、オートコンプリートによる意図しない消去を食らったりすることになるでしょう。ストレスがマッハになること請け合いです (-_-;):
PS> [System.Reflection.Assembly]::LoadFrom((dir .\FinalMockingMigration.exe).FullName)
GAC Version Location
--- ------- --------
False v4.0.30319 C:\Users\Akira\Documents\Visual Studio 2013\Projects\JustMockMigrationDemo\FinalMockingMigrati...
PS> $asmInfo = [System.Reflection.Assembly]::LoadFrom((dir .\FinalMockingMigration.exe).FullName)
PS>
view raw 05_05.ps1 hosted with ❤ by GitHub

GetTypes で型を確認すると、上から 3 つが対象ということがわかります。それらに対し、前のサンプルと同様のフィルタを掛けてみましょう:
PS> $asmInfo.GetTypes()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False Foo System.Object
False True EchoEventHandler System.MulticastDelegate
True False FooGeneric System.Object
False False Program System.Object
PS> ($asmInfo.GetTypes())[0..2] | % { $_.GetMembers() } | ? { $_ -is [System.Reflection.MethodBase] } | ? { !$_.IsAbstract } | ? { $_.DeclaringType -eq $_.ReflectedType }
Method
------
Int32 Execute(Int32, Int32)
Int32 Execute(Int32)
Int32 Echo(Int32)
System.String get_FooProp()
Void set_FooProp(System.String)
Void add_OnEchoCallback(EchoEventHandler)
Void remove_OnEchoCallback(EchoEventHandler)
Void .ctor()
Void Invoke(Boolean)
System.IAsyncResult BeginInvoke(Boolean, System.AsyncCallback, System.Object)
Void EndInvoke(System.IAsyncResult)
Void .ctor(System.Object, IntPtr)
TRet Echo[T,TRet](T)
Void .ctor()
PS>
view raw 05_06.ps1 hosted with ❤ by GitHub

結果が良さそうであれば、間接スタブ設定に変換し、クリップボードにコピーします。なお、解析が終わった Assembly を解放したい場合は、ネストしたプロンプトを終了すれば OK です。
PS> ($asmInfo.GetTypes())[0..2] | % { $_.GetMembers() } | ? { $_ -is [System.Reflection.MethodBase] } | ? { !$_.IsAbstract } | ? { $_.DeclaringType -eq $_.ReflectedType } | pget | clip
PS> exit
PS>
view raw 05_07.ps1 hosted with ❤ by GitHub

Visual Studio に戻り、FinalMockingMigration.v4.0.30319.v1.0.0.0.prig に間接スタブ設定を貼り付けます。ビルドは通りましたか?それでは、ちょっと長くなりましたが、実際の移行を行っていきましょう!





Assert Final Method Setup
Fakes のサンプルでも言いましたが、Prig は Mock Object としての機能を持っていませんので、Moq のような別のモックフレームワークと一緒に使うことを推奨しています。しかし、どうも JustMock のサンプルは、単に Test Stub としての振る舞いが紹介されているだけのものが大半ですので、Prig のサンプルもそれと一貫性を持たせるようにしました:

はじめに、final メソッドの戻り値を入れ替えてみましょう:
[Test]
public void Prig_should_setup_a_call_to_a_final_method()
{
using (new IndirectionsContext())
{
// Arrange
var fooProxy = new PProxyFoo();
fooProxy.EchoInt32().Body = (@this, arg1) => 10;
var foo = (Foo)fooProxy;
// Act
var actual = foo.Echo(1);
// Assert
Assert.AreEqual(10, actual);
}
}
view raw 05_08.cs hosted with ❤ by GitHub

fooProxy.EchoInt32().Body = (@this, arg1) => 10; で、Prig は、その final メソッドを、定数 10 を返す処理に入れ替えています。大丈夫ですよね。次、行ってみましょう!





Assert Property Get
final プロパティを入れ替えるサンプルです:
[Test]
public void Prig_should_setup_a_call_to_a_final_property()
{
using (new IndirectionsContext())
{
// Arrange
var fooProxy = new PProxyFoo();
fooProxy.FooPropGet().Body = @this => "bar";
var foo = (Foo)fooProxy;
// Act
var actual = foo.FooProp;
// Assert
Assert.AreEqual("bar", actual);
}
}
view raw 05_09.cs hosted with ❤ by GitHub

fooProxy.FooPropGet().Body = @this => "bar"; で、Prig は、その final プロパティを、定数 bar を返す処理に入れ替えています。どんどん行きますよ (^O^)





Assert Property Set
final プロパティのセッターを検証するサンプルです。これは Moq を使ったほうが簡単そうですね:
[Test]
[ExpectedException(typeof(MockException))]
public void Prig_should_assert_property_set()
{
using (new IndirectionsContext())
{
// Arrange
var fooProxy = new PProxyFoo();
var fooPropSetMock = new Mock<IndirectionAction<Foo, string>>(MockBehavior.Strict);
fooPropSetMock.Setup(_ => _(fooProxy, "ping"));
fooProxy.FooPropSetString().Body = fooPropSetMock.Object;
var foo = (Foo)fooProxy;
// Act, Assert
foo.FooProp = "foo";
}
}
view raw 05_10.cs hosted with ❤ by GitHub

Telerik.JustMock.Behavior.Strict は、Moq.MockBehavior.Strict にあたります。従って、Setup で指定した条件を満たさない引数で対象のメンバーを呼び出した場合、MockException をスローするようになります。





Assert Method Overloads
各 final メソッドのオーバーロードを入れ替えてみましょう。間接スタブ設定を正しく追加していれば、特に迷うことは無いはずです:
[Test]
public void Prig_should_assert_on_method_overload()
{
using (new IndirectionsContext())
{
// Arrange
var fooProxy = new PProxyFoo();
fooProxy.ExecuteInt32().Body = (@this, arg1) => arg1;
fooProxy.ExecuteInt32Int32().Body = (@this, arg1, arg2) => arg1 + arg2;
var foo = (Foo)fooProxy;
// Act, Assert
Assert.AreEqual(1, foo.Execute(1));
Assert.AreEqual(2, foo.Execute(1, 1));
}
}
view raw 05_11.cs hosted with ❤ by GitHub

特に問題は無いですよね?次に進みましょう。





Assert Method Callbacks
イベントとしてのコールバックを置換することを説明する例です。Moles の例で紹介しましたが、実装には若干のテクニックが必要です:
[Test]
public void Prig_should_assert_on_method_callbacks()
{
using (new IndirectionsContext())
{
// Arrange
var handler = default(Foo.EchoEventHandler);
var fooProxy = new PProxyFoo();
fooProxy.AddOnEchoCallbackEchoEventHandler().Body = (@this, value) => handler += value;
fooProxy.EchoInt32().Body = (@this, arg1) => { handler(true); return arg1; };
var foo = (Foo)fooProxy;
var called = false;
foo.OnEchoCallback += echoed => called = echoed;
// Act
foo.Echo(10);
// Assert
Assert.IsTrue(called);
}
}
view raw 05_12.cs hosted with ❤ by GitHub

実装は以下の流れになります:
  1. fooProxy.AddOnEchoCallbackEchoEventHandler().Body = (@this, value) => handler += value; のように、入れ替えたいイベントの += 演算子を乗っ取り、渡されたハンドラ(value)をテストのためのデリゲート(handler)に紐付ける。
  2. 元のイベントを発行したいタイミングで、手順 1 で紐づけたデリゲート(handler)を代わりに実行する。
これで、元のイベントを発火することと同じ効果を得ることができるようになります。





Assert Generic Types and Methods
ジェネリックな型とメソッドを入れ替える例です:
[Test]
public void Prig_should_assert_on_generic_types_and_method()
{
using (new IndirectionsContext())
{
// Arrange
var expected = "ping";
var fooGenericProxy = new PProxyFooGeneric();
fooGenericProxy.EchoOfTOfTRetT<string, string>().Body = (@this, s) => s;
var fooGeneric = (FooGeneric)fooGenericProxy;
// Act
var actual = fooGeneric.Echo<string, string>(expected);
// Assert
Assert.AreEqual(expected, actual);
}
}
view raw 05_13.cs hosted with ❤ by GitHub

ね、簡単でしょう?これに関しては、以前の記事で解説したジェネリックのサポートも、合わせてご覧いただければと思います。



2014年11月23日日曜日

デフォルトの振る舞い - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 4 段!(元ネタ:Prigwiki より、FEATURES: Default Behavior。同シリーズの他記事:1 2 3

自動化されたテストで既存の振る舞いを囲う時、そのメソッドが呼び出されているかどうかだけを、単にチェックしたくなることがあると思います。例えば、「対象のメソッドが、現在の環境やプラットフォームの情報にアクセスしているかどうかを知りたいので、クラス Environment のいずれかのメソッドの呼び出しを検出したい」というようなことが考えられますね。これを実現するために、Prig では間接スタブのデフォルトの振る舞いを変更する機能をサポートしています。今回はこの機能を解説していきましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Muhammad Shujaat Siddiqi Prefer 32-Bit with Any CPU Platform Target - Visual Studio 2012 Enhancements
何かのときにすっと出したい、プログラミングに関する法則・原則一覧 by @hiroki_daichi on @Qiita
OpenTouryoProject/OpenTouryo - GitHub
Code Generation in a Build Process
Microsoft.Fakes stub interface fails to be found - Stack Overflow
Microsoft Fakes: Trying to shim a class but dependencies are still there - Stack Overflow
c# - How do you call a constructor via an expression tree on an existing object? - Stack Overflow
Why did we build
rspec の書き方がわからない"" - Togetterまとめ
Myron Marston » Notable Changes in RSpec 3





目次

デフォルトの振る舞い
例として、以下のような「古き良き時代の」コードに対して、仕様の変更が発生した、ということを想像してみてください - もちろん、テストコードはありませんね (´・ω・`)
public struct CommunicationContext
{
public int VerifyRuntimeVersion(string[] args)
{
...(snip)...
}
public int VerifyPrereqOf3rdParty(string[] args)
{
...(snip)...
}
public int VerifyUserAuthority(string[] args)
{
...(snip)...
}
public int VerifyProductLicense(string[] args)
{
...(snip)...
}
}
public static class JobManager
{
public static void NotifyStartJob(CommunicationContext ctx, string[] args)
{
int err = 0;
Log("前提条件の検証を開始します。");
if ((err = ctx.VerifyRuntimeVersion(args)) != 0)
goto fail;
if ((err = ctx.VerifyPrereqOf3rdParty(args)) != 0)
goto fail;
if ((err = ctx.VerifyUserAuthority(args)) != 0)
goto fail;
if ((err = ctx.VerifyProductLicense(args)) != 0)
goto fail;
Log("前提条件の検証を終了します。");
Log("連携パラメータの構築を開始します。");
int mode = 0;
// … mode を計算するための大量のコードがある想定 …
if (err != 0)
goto fail;
bool notifiesError = false;
// … notifiesError を計算するための大量のコードがある想定 …
if (err != 0)
goto fail;
string hash = null;
// … hash を計算するための大量のコードがある想定 …
if (err != 0)
goto fail;
Log(string.Format("連携パラメータの構築を終了します。" +
"code: {0}, mode: {1}, notifiesError: {2}, hash: {3}", err, mode, notifiesError, hash));
UpdateJobParameterFile(err, mode, notifiesError, hash);
return;
fail:
Log(string.Format("連携の通知に失敗しました。code: {0}, {1}:{2} 場所 {3}",
err, Environment.MachineName, Environment.CurrentDirectory, Environment.StackTrace));
UpdateJobParameterFile(err, 0, false, null);
}
public static void UpdateJobParameterFile(int code, int mode, bool notifiesError, string hash)
{
...(snip)...
}
static void Log(string msg)
{
...(snip)...
}
}
view raw 04_01.cs hosted with ❤ by GitHub

NotifyStartJob は、最初に、あるジョブを実行するための前提条件を検証し、全ての条件が満たされていれば、ジョブに引き渡すパラメータファイルを生成します。goto 文を使って、共通のエラー処理に飛ばす手法は、C 言語が主流だった時代によく見られたものですが、例外による通知が標準になった現在でも、そのエラーが本当の例外でなく単に業務的なエラーだったり、パフォーマンスの問題があったりすれば、まだ利用されることがあるかもしれません。

ちょっと簡単ではないということを感じたあなたは、テストコードを書くことにしました。まずは、古くからあるモックフレームワークとちょっとの修正で可能な範囲で、その振る舞いを囲うことにします。プロダクトコードを見回すと、CommunicationContext は構造体である必要はなさそう。クラスに変更し、メンバーを virtual にすることにしました:
public class CommunicationContext
{
public virtual int VerifyRuntimeVersion(string[] args)
{
...(snip)...
}
public virtual int VerifyPrereqOf3rdParty(string[] args)
{
...(snip)...
}
public virtual int VerifyUserAuthority(string[] args)
{
...(snip)...
}
public virtual int VerifyProductLicense(string[] args)
{
...(snip)...
}
}
view raw 04_02.cs hosted with ❤ by GitHub

メンバーが static であることも止めたいですが、それはできません。なぜなら、他の多くのメンバから参照されてしまっているためです。仕方がないので、ここで、Prig を使い間接スタブを作成することにします。Package Manager Console で、以下のコマンドを実行してスタブ設定を作成し・・・:
PM> Add-PrigAssembly -AssemblyFrom <JobManager の Assembly へのフルパス。例えば、"C:\Users\User\GofUntestable\GofUntestable\bin\Debug\GofUntestable.exe">
view raw 04_03.cs hosted with ❤ by GitHub

以下のコマンドで、UpdateJobParameterFile の設定をクリップボードにコピーし、スタブ設定(例えば、GofUntestable.v4.0.30319.v1.0.0.0.prig)に追加します:
PM> Add-Type -Path <JobManager の Assembly へのフルパス。例えば、"C:\Users\User\GofUntestable\GofUntestable\bin\Debug\GofUntestable.exe">
PM> Find-IndirectionTarget ([<JobManager のフルネーム。例えば、GofUntestable.JobManager>]) UpdateJobParameterFile | Get-IndirectionStubSetting | Clip
view raw 04_04.ps1 hosted with ❤ by GitHub

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="prig" type="Urasandesu.Prig.Framework.PilotStubberConfiguration.PrigSection, Urasandesu.Prig.Framework" />
</configSections>
<prig>
<stubs>
<add name="UpdateJobParameterFileInt32Int32BooleanString" alias="UpdateJobParameterFileInt32Int32BooleanString">
<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="">UpdateJobParameterFile</Name>
<AssemblyName z:Id="3" z:Type="System.String" z:Assembly="0" xmlns="">GofUntestable</AssemblyName>
<ClassName z:Id="4" z:Type="System.String" z:Assembly="0" xmlns="">GofUntestable.JobManager</ClassName>
<Signature z:Id="5" z:Type="System.String" z:Assembly="0" xmlns="">Void UpdateJobParameterFile(Int32, Int32, Boolean, System.String)</Signature>
<Signature2 z:Id="6" z:Type="System.String" z:Assembly="0" xmlns="">System.Void UpdateJobParameterFile(System.Int32, System.Int32, System.Boolean, System.String)</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 04_05.xml hosted with ❤ by GitHub

上記の準備によって、全てのテストが書けるようになります。例えば、以下のようなケースが書けると思います:
[TestFixture]
public class JobManagerTest
{
[Test]
public void NotifyStartJob_should_verify_using_the_methods_of_CommunicationContext_then_UpdateJobParameterFile()
{
using (new IndirectionsContext())
{
// Arrange
var mockCtx = new Mock<CommunicationContext>();
mockCtx.Setup(_ => _.VerifyRuntimeVersion(It.IsAny<string[]>())).Returns(0);
mockCtx.Setup(_ => _.VerifyPrereqOf3rdParty(It.IsAny<string[]>())).Returns(0);
mockCtx.Setup(_ => _.VerifyUserAuthority(It.IsAny<string[]>())).Returns(0);
mockCtx.Setup(_ => _.VerifyProductLicense(It.IsAny<string[]>())).Returns(0);
var ctx = mockCtx.Object;
var args = new[] { "foo", "bar", "baz", "qux" };
var mockUpdateJobParameterFile = new Mock<IndirectionAction<int, int, bool, string>>();
PJobManager.UpdateJobParameterFileInt32Int32BooleanString().Body = mockUpdateJobParameterFile.Object;
// Act
JobManager.NotifyStartJob(ctx, args);
// Assert
mockCtx.Verify(_ => _.VerifyRuntimeVersion(args), Times.Once());
mockCtx.Verify(_ => _.VerifyPrereqOf3rdParty(args), Times.Once());
mockCtx.Verify(_ => _.VerifyUserAuthority(args), Times.Once());
mockCtx.Verify(_ => _.VerifyProductLicense(args), Times.Once());
mockUpdateJobParameterFile.Verify(_ => _(0, 0, false, null), Times.Once());
}
}
}
view raw 04_06.cs hosted with ❤ by GitHub

ところで、今回の仕様変更は何だったかというと、「ライセンスによる検証を削除する」というものでした。具体的には、「VerifyProductLicense はもう呼ばれない」ということを実装すれば良いことになります。以下のようにテストの Assert の一部を書き換え、テストが失敗することを確認します:
...(snip)...
// Assert
mockCtx.Verify(_ => _.VerifyRuntimeVersion(args), Times.Once());
mockCtx.Verify(_ => _.VerifyPrereqOf3rdParty(args), Times.Once());
mockCtx.Verify(_ => _.VerifyUserAuthority(args), Times.Once());
// VerifyProductLicense はもう呼ばれませんので、検証を Once() から Never() に変更します。
mockCtx.Verify(_ => _.VerifyProductLicense(args), Times.Never());
mockUpdateJobParameterFile.Verify(_ => _(0, 0, false, null), Times.Once());
...(snip)...
view raw 04_07.cs hosted with ❤ by GitHub

あとは、テストが通るようにプロダクトコードを変更するだけですね!あなたは安心して、JobManager を以下のように書き換え、テストが成功することを確認します。簡単な仕事でしたね?
...(snip)...
public static void NotifyStartJob(CommunicationContext ctx, string[] args)
{
int err = 0;
Log("前提条件の検証を開始します。");
if ((err = ctx.VerifyRuntimeVersion(args)) != 0)
goto fail;
if ((err = ctx.VerifyPrereqOf3rdParty(args)) != 0)
goto fail;
if ((err = ctx.VerifyUserAuthority(args)) != 0)
goto fail;
// 製品ライセンスはもうチェックする必要はない。
//if ((err = ctx.VerifyProductLicense(args)) != 0)
goto fail;
Log("前提条件の検証を終了します。");
Log("連携パラメータの構築を開始します。");
int mode = 0;
...(snip)...
UpdateJobParameterFile(err, mode, notifiesError, hash);
return;
fail:
Log(string.Format("連携の通知に失敗しました。code: {0}, {1}:{2} 場所 {3}",
err, Environment.MachineName, Environment.CurrentDirectory, Environment.StackTrace));
UpdateJobParameterFile(err, 0, false, null);
}
view raw 04_08.cs hosted with ❤ by GitHub

・・・アイヤー、これはヒドイ。問題に気づきましたか?これでは常に goto fail; へ行ってしまい、ジョブに渡すパラメータファイルが常に作成されてしまいます!加えて、残念なことに、JobManager は err を code として、UpdateJobParameterFile に 設定していますので、最悪ジョブが常に実行されることになるでしょう。今年 2 月にあった Apple 史上最悪のセキュリティバグが思い出されますね。

このような問題を防止するには、意図しないメソッドが呼び出されたら例外を投げるようにします。この例では、fail ラベルで、問題があった環境の情報をログに書き込む処理があり、Environment の各プロパティを参照しているようです。従って、それらが呼び出された時に例外をスローするのが良いでしょう。Environment は mscorlib に属すクラスで、ほとんどのメンバが static です。この場合、Prig を使って間接スタブを作成する以外に、選択の余地はありませんね:
PM> Add-PrigAssembly -Assembly "mscorlib, Version=4.0.0.0"
view raw 04_09.ps1 hosted with ❤ by GitHub

上のコマンドを実行すると、mscorlib の間接スタブ設定を作成することができます。そうしましたら、Environment の public static なメンバーの間接スタブを作成しましょう(以下のコマンドの結果を mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます):
PM> [System.Environment].GetMembers([System.Reflection.BindingFlags]'Public, Static') | ? { $_ -is [System.Reflection.MethodInfo] } | Get-IndirectionStubSetting | Clip
view raw 04_10.ps1 hosted with ❤ by GitHub

さて、これで Environment の public static なメンバーに対して、一度にデフォルトの振る舞いを変更できるようになりました。テストコードに、以下の変更を加えます:
...(snip)...
[Test]
public void NotifyStartJob_should_verify_using_the_methods_of_CommunicationContext_then_UpdateJobParameterFile()
{
using (new IndirectionsContext())
{
// Arrange
// Environment の大体のメソッドのデフォルトの振る舞いを設定します。
PEnvironment.
ExcludeGeneric().
// Environment.CurrentManagedThreadId は、Mock<T>.Setup<TResult>(Expression<Func<T, TResult>>) で使われているため、除外しておきましょう。
Exclude(PEnvironment.CurrentManagedThreadIdGet()).
// Environment.OSVersion は、Times.Once() で使われているため、除外しておきましょう。
Exclude(PEnvironment.OSVersionGet()).
DefaultBehavior = IndirectionBehaviors.NotImplemented;
var mockCtx = new Mock<CommunicationContext>();
mockCtx.Setup(_ => _.VerifyRuntimeVersion(It.IsAny<string[]>())).Returns(0);
mockCtx.Setup(_ => _.VerifyPrereqOf3rdParty(It.IsAny<string[]>())).Returns(0);
...(snip)...
view raw 04_11.cs hosted with ❤ by GitHub

使用しているモックフレームワークによっては、既に予約されているメンバーがあるため、Environment のいくつかのメンバーを除外することがちょっとメンドクサイ(Moq のケースだと、上記の通り、Environment.CurrentManagedThreadId や Environment.OSVersion がそれに当たります)。ですが、これで意図しないメソッドの呼び出しを監視することができるようになりました。NotifyStartJob に対する上記の修正では、もはやテストが通らないことがわかると思います。これで本当に安全になったわけですね!

ちなみにこの機能、テストケースを書く時、モックのされ具合を確認することや、複雑な条件下にある外部アクセスから守るガードとしても利用ができます。また、IndirectionBehaviors.DefaultValue を設定すれば、デフォルト値を返すようなデフォルトの振る舞いに変更することもできます。レガシーコードにおいては、1 つのメソッドで何度も何度も void Foo() のようなシグネチャのメソッドが呼び出されている状況(中でメンバ変数 or グローバル変数ガシガシ変えてる!!!)も少なくないでしょう。しかし、テスト中のもの以外を何もしない振る舞いに設定すれば、簡単に観点を絞り込むことができるようになるはずです。



2014年11月19日水曜日

移行サンプル:Microsoft Fakes による HttpWebRequest のモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 3 段!(元ネタ:Prigwiki より、MIGRATION: Mocking HttpWebRequest by Microsoft Fakes。同シリーズの他記事:1 2

今回は、Microsoft Fakes から Prig への移行サンプルを解説させていただきますね。Fakes の解説ドキュメント「Better Unit Testing with Microsoft Fakes」の章「Migrating from commercial and open source frameworks」に載せられているマイグレーションの説明を、例として挙げています。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
An Introduction To RSpec | Treehouse Blog
Ruby - refinementsについてグダグダ - Qiita
Medihack » Blog Archive » Intend to extend (metaprogramming in Ruby)
c# - How does WCF deserialization instantiate objects without calling a constructor - Stack Overflow
Runtime method hooking in Mono - Stack Overflow
.NET CLR Injection: Modify IL Code during Run-time - CodeProject
CLR Injection: Runtime Method Replacer - CodeProject
Moles: Tool-Assisted Environment Isolation with Closures - Microsoft Research





目次

移行サンプル:Microsoft Fakes による HttpWebRequest のモック化
テスト対象はこんな感じ(警告が発生していたため、若干修正してあります):
using System.Net;
namespace FakesMigrationDemo
{
public class WebServiceClient
{
public bool CallWebService(string url)
{
var request = CreateWebRequest(url);
var isValid = true;
try
{
var response = request.GetResponse() as HttpWebResponse;
isValid = HttpStatusCode.OK == response.StatusCode;
}
catch
{
isValid = false;
}
return isValid;
}
static HttpWebRequest CreateWebRequest(string url)
{
var request = WebRequest.Create(url) as HttpWebRequest;
request.ContentType = "text/xml;charset=\"utf-8\"";
request.Method = "GET";
request.Timeout = 1000;
request.Credentials = CredentialCache.DefaultNetworkCredentials;
return request;
}
}
}
view raw 03_01.cs hosted with ❤ by GitHub

個人的に、副作用への入力を検証していなかったり、テストメソッド名に期待値が含まれていなかったりで、元の例はいただけません。それはさておき、とりあえずビルドが成功するぐらいには Prig へ移行してみましょう。なお、プロジェクトの Assembly 参照設定は Moles の移行サンプルと同様ですので、詳細はそちらをご参照ください。

Fakes の「Shim」と呼ばれるクラスは、Prig の「Indirection Stub」に当たります。命名規則によって、「Shim」として利用されているクラスは、HttpWebRequest、WebRequest そして HttpWebResponse ということがわかります。これらのクラスは全てアセンブリ System に属していますので、以下のコマンドで間接設定を追加してください:
PM> padd -as "System, Version=4.0.0.0"
view raw 03_02.ps1 hosted with ❤ by GitHub

間接設定 System.v4.0.30319.v4.0.0.0.prig が追加されたら、以下のコマンドを実行することによって作成される結果を貼り付けます:
PM> $targets = [System.Net.HttpWebRequest].GetMembers() | ? { $_ -is [System.Reflection.MethodBase] } | ? { !$_.IsAbstract } | ? { $_.DeclaringType -eq $_.ReflectedType }
PM> $targets += [System.Net.WebRequest].GetMembers() | ? { $_ -is [System.Reflection.MethodBase] } | ? { !$_.IsAbstract } | ? { $_.DeclaringType -eq $_.ReflectedType }
PM> $targets += [System.Net.HttpWebResponse].GetMembers() | ? { $_ -is [System.Reflection.MethodBase] } | ? { !$_.IsAbstract } | ? { $_.DeclaringType -eq $_.ReflectedType }
PM> $targets | pget | clip
view raw 03_03.ps1 hosted with ❤ by GitHub

? { !$_.IsAbstract } は、実装を持っていないメソッドを除外するフィルター、$_.DeclaringType -eq $_.ReflectedType はベースメソッドをオーバーライドするメソッドを除外するフィルターです。説明のために非常にざっくりとしたフィルターを掛けていますが、厳密なフィルターで最小限の設定を作成することをオススメします。広範囲に影響が出てしまうため、必要以上のメソッドを交換可能にしておくことは良いことではありません。

貼り付けたスタブ設定に対してビルドが正常に終了したら、こんな感じでテストが書けると思います:
using FakesMigrationDemo;
using NUnit.Framework;
using System.Net;
using System.Net.Prig;
using Urasandesu.Prig.Framework;
namespace FakesMigrationDemoTest
{
[TestFixture]
public class WebServiceClientTest
{
[Test]
public void TestThatServiceReturnsAForbiddenStatuscode()
{
// ShimsContext.Create() の代わりに、new IndirectionsContext() を使います。
using (new IndirectionsContext())
{
// Arrange
// 特定のインスタンスに対しては、PProxy で始まる名前の間接スタブを使います。
var requestProxy = new PProxyHttpWebRequest();
// 間接スタブ PProxy… は Fakes 同様、暗黙変換演算子を持っていますので、以下のような記述が可能です:
PWebRequest.CreateString().Body = uri => requestProxy;
// Fakes と違い、Prig では、インスタンスメソッドの第 1 引数として、暗黙的に this が引き渡されます。
requestProxy.GetResponse().Body = this1 =>
{
var responseProxy = new PProxyHttpWebResponse();
responseProxy.StatusCodeGet().Body = this2 => HttpStatusCode.Forbidden;
return responseProxy;
};
// Fakes と違い、Prig はデフォルトで元のメソッドを呼び出そうとします。
// もしスタブに何もさせたくないのであれば、デフォルトの振る舞いを以下のようにして変更してください:
requestProxy.ExcludeGeneric().DefaultBehavior = IndirectionBehaviors.DefaultValue;
var client = new WebServiceClient();
var url = "testService";
var expectedResult = false;
// Act
bool actualresult = client.CallWebService(url);
// Assert
Assert.AreEqual(expectedResult, actualresult);
}
}
}
}
view raw 03_04.cs hosted with ❤ by GitHub

ところで、この「Migrating from commercial and open source frameworks」、Fakes は、モックオブジェクトとしての機能を持っていませんので、Moq はそのまま使ったほうが簡単になったんじゃないかなと思うんですが・・・うーむ (-_-;)




付録
個人的にいただけなかった部分を Moq によって修正した例です:
using FakesMigrationDemo;
using FakesMigrationDemoTest.FakesMigrationDemoTestDetail;
using Moq;
using NUnit.Framework;
using System;
using System.Linq.Expressions;
using System.Net;
using System.Net.Prig;
using System.Reflection;
using Urasandesu.Prig.Framework;
namespace FakesMigrationDemoTest
{
[TestFixture]
public class WebServiceClientTest
{
[Test]
public void CallWebService_should_return_false_if_HttpStatusCode_is_Forbidden()
{
using (new IndirectionsContext())
{
// Arrange
var requestProxy = new PProxyHttpWebRequest();
requestProxy.ExcludeGeneric().DefaultBehavior = IndirectionBehaviors.DefaultValue;
var responseProxy = new PProxyHttpWebResponse();
responseProxy.StatusCodeGet().Body = @this => HttpStatusCode.Forbidden;
requestProxy.GetResponse().Body = @this => responseProxy;
// 意図しない変更に対する堅牢性を上げるには、Moq により副作用への入力を検証すべきです。
// 例を挙げると、元のテストは、以下のような意図しない修正をプロダクトコードに加えてしまったとしても(例えば、
// デバッグのために書き換えた固定値が間違って残ったままになっていても)、テストはパスしてしまうでしょう:
// var request = CreateWebRequest(url); ⇒ var request = CreateWebRequest("Foo");
var webRequestMock = new Mock<IndirectionFunc<string, WebRequest>>();
webRequestMock.Setup(_ => _(It.IsAny<string>())).Returns(requestProxy);
PWebRequest.CreateString().Body = webRequestMock.Object;
var client = new WebServiceClient();
var url = "testService";
// Act
var actual = client.CallWebService(url);
// Assert
Assert.IsFalse(actual);
webRequestMock.Verify(_ => _(url), Times.Once());
}
}
[Test]
public void CallWebService_should_set_HttpWebRequest_to_request_textxml_content()
{
using (new IndirectionsContext())
{
// Arrange
// また、Moq を使うのであれば、HttpWebRequest が意図した値に設定されているかどうかも検証すべきでしょう:
var requestProxy = new PProxyHttpWebRequest();
var mocks = new MockRepository(MockBehavior.Default);
mocks.Create(requestProxy.ContentTypeSetString(), _ => _.Body).Setup(_ => _(requestProxy, "text/xml;charset=\"utf-8\""));
mocks.Create(requestProxy.MethodSetString(), _ => _.Body).Setup(_ => _(requestProxy, "GET"));
mocks.Create(requestProxy.TimeoutSetInt32(), _ => _.Body).Setup(_ => _(requestProxy, 1000));
mocks.Create(requestProxy.CredentialsSetICredentials(), _ => _.Body).Setup(_ => _(requestProxy, CredentialCache.DefaultNetworkCredentials));
var responseProxy = new PProxyHttpWebResponse();
responseProxy.StatusCodeGet().Body = @this => HttpStatusCode.OK;
requestProxy.GetResponse().Body = @this => responseProxy;
PWebRequest.CreateString().Body = @this => requestProxy;
var client = new WebServiceClient();
var url = "testService";
// Act
var actual = client.CallWebService(url);
// Assert
Assert.IsTrue(actual);
mocks.VerifyAll();
}
}
}
namespace FakesMigrationDemoTestDetail
{
// デリゲートがベースになっているため、Mock に指定する型が五月蝿くなりがちです。
// 例えば、以下のような拡張メソッドを作成しておくと、コンパイラに推論させることが可能になると思います。
public static class MockRepositoryMixin
{
public static Mock<TMock> Create<TZZ, TMock>(this MockRepository repo, TZZ zz, Expression<Func<TZZ, TMock>> indirection) where TMock : class
{
var mock = repo.Create<TMock>();
((PropertyInfo)((MemberExpression)indirection.Body).Member).SetValue(zz, mock.Object);
return mock;
}
}
}
}
view raw 03_05.cs hosted with ❤ by GitHub




さあ、次!次!
実際のところ、現状で Fakes を導入できている幸運な方は、そのままお使いいただければ良いと思います (´・_・`)

・・・一応機能的な利点を挙げるとすれば、Prig は、Fakes には無い、構造体のコンストラクト時差し替えや、サードパーティ製プロファイリングツールとの連携既存ライブラリにある、シグネチャに非公開な型を持つメソッドの入れ替えをサポートしていますが、まあ些細なことでしょう。
どちらかと言うと、OSS であるが故に、一部のニンゲンは Premium 使えるんだけど、他は Professional なんだよ?、とか CI 環境にまで Premium 以上の Visual Studio 入れなあかんの?、とか、テストだけじゃなく、例えば何か動的に処理を入れ替える仕組みを一時的に入れて開発効率を上げるみたいな、色んなことに使いたいんだけど?とか、そもそもどういう仕組みで動いてるの?とかの状況の方には、ご提案できるやもしれません・・・が、特殊なケースでしょうね。

まあ、本来はテスト向けのツールとして始めたわけじゃなかったですから・・・(震え声)

この辺は一段落したら追々考えるとして、とりあえず、次、行ってみよう! (・∀・)



2014年11月18日火曜日

移行サンプル:Microsoft Research Moles による非同期パターンのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 2 段!(元ネタ:Prigwiki より、MIGRATION: Mocking Asynchronous Pattern by Microsoft Research Moles。同シリーズの他記事:1

今回は、『neue cc - Rx + MolesによるC#での次世代非同期モックテスト考察』で解説されているような、イベントベースの非同期パターンを採用するクラスに対して Microsoft Research Moles でモック化するサンプルを Prig(と Moq)に移行してみましょう。前述のページをキーワード「ShowGoogle」で検索すると、例を見つけられると思います。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
James Dibble - Blog - Microsoft Fakes Part Two
Code rant: How To Add Images To A GitHub Wiki
トンネルの思い出 - 東広島市自然研究会
IronRuby.net
Can we use fakes to test AutomationElement methods
Which Isolation frameworks have you used in the past 3 months?
Battle of the mocking frameworks by @dhelper #mockingframework #softwaredevelopment
djUnit
unit testing - HOW to use MS Fakes shims with NSubstitute mocks? - Stack Overflow





目次

移行サンプル:Microsoft Research Moles による非同期パターンのモック化
プロジェクト MolesMigrationDemo に、メソッド ShowGoogle を持つ ULWebClient を作成し、そのテストプロジェクト MolesMigrationDemoTest を作成します:



using System;
using System.Net;
namespace MolesMigrationDemo
{
public class ULWebClient
{
public static void ShowGoogle()
{
var client = new WebClient();
client.DownloadStringCompleted += (sender, e) =>
{
Console.WriteLine(e.Result);
};
client.DownloadStringAsync(new Uri("http://google.co.jp/"));
}
}
}
view raw 02_01.cs hosted with ❤ by GitHub

MolesMigrationDemoTest に NUnit への参照を追加し、README.md に従って Prig をインストールします。あ、Moq の追加を忘れずに:


mscorlib に属す Console、System に属す WebClient、DownloadStringCompletedEventArgs のモック化を有効にする必要がありますので、以下のコマンドを実行し、間接スタブ設定を作成していきましょう:
PM> padd -as "mscorlib, Version=4.0.0.0"
PM> padd -as "System, Version=4.0.0.0"
view raw 02_02.ps1 hosted with ❤ by GitHub

padd -as は、Add-PrigAssembly -Assembly のエイリアスです。このコマンドの実行により、mscorlib.v4.0.30319.v4.0.0.0.prig と System.v4.0.30319.v4.0.0.0.prig がテストプロジェクトに追加されます:



次に、以下のコマンドを実行します。これは、Console のための間接スタブ設定をクリップボードにコピーするという意味ですね。で、mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます:

PM> pfind ([System.Console]) 'WriteLine\(System.String\)' | pget | clip
view raw 02_03.ps1 hosted with ❤ by GitHub

ちなみに、エイリアスについて説明しておくと、pfind は Find-IndirectionTarget、pget は Get-IndirectionStubSetting に当たります。貼り付けた結果はこんな感じ:



同様に、WebClient と DownloadStringCompletedEventArgs の設定を作成します。Get-IndirectionStubSetting は MethodBase の配列を渡しさえすれば、良い感じにスタブ設定を作成してくれますので、PowerShell で、標準のリフレクション API から取得した結果を使い、フィルタリングしても何ら問題はありません:
PM> $targets = [System.Net.WebClient].GetMembers([System.Reflection.BindingFlags]'Public, Instance') | ? { $_ -is [System.Reflection.MethodBase] } | ? { $_.ToString() -match 'DownloadString' }
PM> $targets += [System.Net.DownloadStringCompletedEventArgs].GetMembers([System.Reflection.BindingFlags]'Public, Instance') | ? { $_ -is [System.Reflection.MethodBase] } | ? { $_.ToString() -match 'Result' }
PM> $targets | pget | clip
view raw 02_04.ps1 hosted with ❤ by GitHub

そうしたら、次のように System.v4.0.30319.v4.0.0.0.prig へ結果を貼り付けます:



さて、ビルドは通りましたか?そうであれば、間接スタブを使うことができます。以下のように、オリジナルの例から移行することができるでしょう:
using MolesMigrationDemo;
using Moq;
using NUnit.Framework;
using System.Net;
using System.Net.Prig;
using System.Prig;
using Urasandesu.Prig.Framework;
namespace MolesMigrationDemoTest
{
[TestFixture]
public class ULWebClientTest
{
[Test]
public void ShowGoogle_should_write_response_from_google_to_standard_output()
{
// Prig には、HostType("Moles") のような属性はありません。 using (new IndirectionsContext()) を代わりに使ってください。
using (new IndirectionsContext())
{
// Arrange
var handler = default(DownloadStringCompletedEventHandler);
handler = (sender, e) => { };
// 全てのインスタンスのメンバーをモック化する Moles の AllInstances のような機能はありません。デフォルトがそのような機能であるためです。
PWebClient.AddDownloadStringCompletedDownloadStringCompletedEventHandler().Body = (@this, value) => handler += value;
PWebClient.RemoveDownloadStringCompletedDownloadStringCompletedEventHandler().Body = (@this, value) => handler -= value;
PWebClient.DownloadStringAsyncUri().Body = (@this, address) =>
{
// 特定のインスタンスに対してモック化を行いたい場合は、PProxy で始まるスタブを使います。
var e = new PProxyDownloadStringCompletedEventArgs();
e.ResultGet().Body = @this_ => "google!modoki";
handler(@this, e);
};
var mockWriteLine = new Mock<IndirectionAction<string>>();
mockWriteLine.Setup(_ => _(It.IsAny<string>()));
PConsole.WriteLineString().Body = mockWriteLine.Object;
// Act
ULWebClient.ShowGoogle();
// Assert
mockWriteLine.Verify(_ => _("google!modoki"), Times.Once());
}
}
}
}
view raw 02_05.cs hosted with ❤ by GitHub




さっくりとまとめ
去年の時点では、まだ私の観測範囲でもバリバリ使われてた感じの Moles。 これのインパクトがあったせいか、.NET で static/final/sealed/拡張メソッド入れ替え可能な Mocking フレームワーク、無償なヤツって無いの?、みたいな話題は、QA サイトに定期的に上がる感じ。残念ながら、今のところ、あまり進展は無い感じですね。

V1.0.0 にもなったことですし、今後は私のほうでも、そのような話題を見つけたら、積極的にアプローチしてみたいと思います。人ばs…改善のアイディアも、頂けるかもしれませんしね。

Prig. It is my weekend project but it is expressed an open source alternative to Microsoft Fakes!! ;)



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 開発のお供として、末永くお付き合いいただければ幸いでございます。



2014年5月31日土曜日

ニセモノ語り - Prig: Open Source Alternative to Microsoft Fakes -

"偽る事が悪い事なら、僕は悪い奴でいいんです。"


プリリリースです!

  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





目次

クイックツアー
試作版スタブ生成器の追加やランナーの追加で導入手順はかなり変わりました。前回も登場した、「テストしにくい副作用を直接参照している処理」を例に見ていきます:
using System;
namespace program1.MyLibrary
{
public static class LifeInfo
{
public static bool IsNowLunchBreak()
{
var now = DateTime.Now;
return 12 <= now.Hour && now.Hour < 13;
}
}
}
view raw LifeInfo.cs hosted with ❤ by GitHub

手順としては以下のような感じ:
Step 1: スタブ設定の作成
Step 2: スタブの生成
Step 3: テストの作成
Step 4: テストの実行
Final Step: リファクタリングしてキレイに!
では、実際にやってみたいと思います。


Step 1: スタブ設定の作成
以下のようなスタブ設定を作成します:

<?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" を実行します:

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 を参照に加えます。
テストコードでは、スタブを利用し、偽の情報を返すテストダブルに置き換えることで、テストが可能になるのです!

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);
}
}
}
}
view raw LifeInfoTest.cs hosted with ❤ by GitHub



Step 4: テストの実行
本来は、プロファイラベースのモックツールを有効にするためには、環境変数を弄る必要があります。Microsoft Fakes/Typemock Isolator/Telerik JustMock は、そのための小さなランナーを提供しますので、Prig でも同様としました。なので、テストを実行するには "prig.exe" を以下のように使用する必要があります:

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



Final Step: リファクタリングしてキレイに!
テストができてしまえば、リファクタリングに躊躇はなくなりますね!例えば、以下のようにリファクタリングできることがわかると思います:

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」(実装の詳細の確認)として例が挙げられていますが、ちょっと難しそうに見えましたので、もう少し簡単なサンプルで見てみます:

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 の資料も、実は効果を見せるために、わざと難しい例にしているのかもしれません!)。サンプルということでとりあえず書いてみましょう。こんな感じで:

・・・
[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 案は、採用しても、失敗したテストを通すことができないことにすぐ気づくと思います。実はこのカウンタ、あまり精度良くないのですよね。簡単に見えて、実は落とし穴がある例でした Ψ(`∀´)Ψ)

ちょっと注意が必要なのが構造体。迂廻処理に、クラスと同じ取り回しで引数を与えると、コピーしたものに対する処理しかできなくなってしまいますので、シグネチャが参照渡しに変わっています。例えば、以下のようなコードがあって、:

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 が初期化されるかを確認したい場合は・・・:
・・・
[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++ の初歩的な知識は必要なのですががが):

・・・
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 との格闘の跡やらで溢れていますので、まだしばらくは、人様に中身を紹介できるものにはならないでしょうね ...( = =)

閑話休題。ジェネリックタイプとジェネリックメソッドの迂廻路の雰囲気を紹介しましょう。

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;
}
}
}


・・・
[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 の解説で使った、懐かしのテストしにくい設定読み込みクラスに登場していただきます。今回は不運にも、それを使う側になったシチュエーションで ヽ(;▽;)ノ

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 を紹介するにあたっては恰好のサンプルにしかなりません。「テストがなければ、仕様を囲うコードを書けば良いじゃない?」

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 を作っておくとちょっとは判りやすくなると思います:

"..\..\..\..\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"
view raw runtests.bat hosted with ❤ by GitHub

で、実行します:

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

ReportGenerator を使って結果を整形するとこんな感じ:






ちゃんと C0/C1 が網羅できていることが確認できました!もうリファクタリングに躊躇することはありません。保守しやすいコードに直し放題ですやったー! (((o(*゚▽゚*)o)))
ちなみにこの仕組み、Fakes が Visual Studio に標準搭載になって除外されてしまった残念な部分でもあります。Moles 時代はできたのに・・・自社製品で囲い込みたい気持ちは分かりますが、うーん (-_-;)





テスト!テスト!!テスト!!!
目に見える使い方は以上な感じ。あとは x86/x64 対応とか、.NET 4 の SxS 実行対応とか、地味な部分も手広くやってはいます。はじめはうんともすんとも言わなかった NUnit GUI での実行も、ここ数か月で動くようになり、安定性はだいぶ上がってきた実感がありますね。が、この解説記事向けのサンプルを書く中で未実装 OpCode が見つかるなど、やっぱりまだまだな状態。さあ、ここからが正念場です。テスト!テスト!!テスト!!!