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 入れなあかんの?、とか、テストだけじゃなく、例えば何か動的に処理を入れ替える仕組みを一時的に入れて開発効率を上げるみたいな、色んなことに使いたいんだけど?とか、そもそもどういう仕組みで動いてるの?とかの状況の方には、ご提案できるやもしれません・・・が、特殊なケースでしょうね。

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

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



0 件のコメント:

コメントを投稿