Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9366

Microsoft Fakesを用いてHttpContextを利用するクラスのテストを行う

$
0
0

概要

Webアプリケーションの開発で、HttpContextから情報を取得するメソッドがあったとする。このメソッドのテストはIISが必要となるため、通常であれば結合テストフェーズにならないとテストができない。しかしどうしても単体テストを行いたい場合には、Visual Studio Enterpriseで利用可能なMS Fakesを利用することで、何とか単体でのテストが可能であるということを、この記事で述べていきたいと思う。

Webアプリケーションのテストに関して

概要では、HttpContextから値を取得するメソッドのテストについて記述すると書いたが、本来であればテスト対象となるようなメソッドがHttpContextからじかに値を取得するような設計があまりよくないと思われる。
つまり、ブラウザから入力した値をコードビハインドやハンドラーで取得せずに後続処理に投げていたり、もしくはセッション状態に常にアクセス可能なオブジェクトを保持し続けるといった実装はクラス間の結合が密になってしまうため、改修やテストの際に扱いにくくなってしまうということである。またレガシーナコードなどでは、ビジネスロジックとしてオブジェクト(モデル)に移譲すべき処理をコードビハインドに直接記述しいるケースも考えられる。

正しく対応するのであれば、サーバー処理に必要な引数はコードビハインドで変数として抜き出して業務ロジックに引き渡すようにする。また、セッションにオブジェクトを保持しなくていいような設計にする。コードビハインドに業務ロジックがあった場合はリファクタリングして別のクラスに処理を委譲するなどプロジェクトのクラス構成から設計しなおした場合が良いことが多いだろう。

だが、レガシーなアプリケーションや、複雑な状態を保持するオブジェクトを何らかの理由(工期やプロジェクトのチーム構成の問題など)で保持する理由ケースが存在することは確かなので、そのようなメソッドをテストしたい場合もあるのが実情である。
では、どうすれば単体テストを実施することが可能なのだろうか。

※ここでは本当にHttpContextを必要としている場合を想定している。HttpContextから抜き出した値を保持している別のクラスからその値を取得する場合などは、その別クラスをインターフェース化しテスト用のスタブを作成するだけで十分テストを実施できる場合が多い。

Microsoft Fakes

Microsoft Fakesは2020年10月現在Visual StudioのEnterprise版でしか利用できないが、アプリケーションの一部の機能(プロパティ、メソッド)をテスト実施時に自分の好きなふるまいをするように置き換えることができるツールである。
(参考:https://docs.microsoft.com/ja-jp/visualstudio/test/isolating-code-under-test-with-microsoft-fakes?view=vs-2019 )
このツールを用いて、HttpContextの返す様々なオブジェクトや値をテスト実施の際に偽装してしまえば、今問題になっているメソッドのテストが行える。

サンプルプロジェクト

今、適当なWebアプリを作成してみる。

image.png

Visual Studio 2019だと、空のソリューションに対してC#>Windows>Webでプロジェクトの種類を絞り込んで、ASP.NET Webアプリケーション(.NET Framework)を選択し、空のプロジェクトを選択作成したうえで、default.aspxとエラーのリダイレクト先のerropage.htmlを追加して作っている。

それぞれのファイルの中身は以下のような感じ

defautl.aspx
<%@PageLanguage="C#"AutoEventWireup="true"CodeBehind="default.aspx.cs"Inherits="WebAppTest._default"%><!DOCTYPEhtml><htmlxmlns="http://www.w3.org/1999/xhtml"><headrunat="server"><metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/><title></title></head><body><formid="form1"runat="server"><div><p>表示用の文字を入力してください</p><p><inputtype="text"name="presentation"/></p><p><buttontype="submit"id="button1">"送信"</button></p></div></form></body></html>
default.aspx.cs
usingSystem;usingSystem.Web;namespaceWebAppTest{publicpartialclass_default:System.Web.UI.Page{protectedvoidPage_Load(objectsender,EventArgse){stringsentence=HttpContext.Current.Request["presentation"];if(!string.IsNullOrEmpty(sentence)){if(sentence=="例外表示"){// Exceptionを投げたいthrownewApplicationException("エラー表示をします");}else{this.Context.Response.Write($"あなたは「{sentence}」と入力しました。");}}}}}
web.config
<system.web>
    <compilationdebug="true"targetFramework="4.8"/>
    <httpRuntimetargetFramework="4.8"/>
    <!-- リダイレクト先を指定 -->
    <customErrorsdefaultRedirect="errorpage.html"mode="On" />
  </system.web>
errorpage.html
<!DOCTYPE html><html><head><metacharset="utf-8"/><title></title></head><body>例外が発生しました。
</body></html>

フォームに入力した文字を加工して表示するだけの単純なアプリで、「例外発生」という文字列が送られてきた時だけエラーページに飛ばすような作りである。
では、このdefault.aspx.csにあるPage_Loadで、本当に「例外発生」という文字列が送られてきた時にApplicationExceptionをスローしているかをテストするにはどうすればいいだろうか。

テストプロジェクトの作成

ソリューションに空のテストプロジェクトを追加して、以下の手順でテストクラスを作成します。

  1. 追加するのはC#>Windows>テスト の中の 単体テストプロジェクト(.NET Framework)
  2. WebAppTestに対してプロジェクト参照を追加
  3. テストする対象がPageオブジェクトの継承クラスなので、System.Web.dllに対するアセンブリ参照を追加
  4. テスト対象メソッドがprotectedメソッドなので、外部から呼び出せるように_default.aspx.csの継承クラスdefaultPageInheritor.csを追加
defaultPageInheritor.cs
usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingWebAppTest;namespaceWebAppTest_Test{/// <summary>/// _default.aspx.csをテストするための継承クラス/// </summary>publicclassdefaultPageInheritor:_default{/// <summary>/// protectedメソッドは直接実行できないので、このメソッドから呼び出す/// </summary>/// <param name="sender"></param>/// <param name="e"></param>publicvoidPage_Load(objectsender,EventArgse){base.Page_Load(sender,e);}}}
  1. テストクラスdefaultTests.csを追加
  2. 必要に応じて以下のテストフレームワークをNuGetからインストールする image.png

テストの実装

Page_Loadのタイミングで例外を投げることをテストしたいので、以下のようなテストコードを記述してみるが、このままだとフォームからの入力がないため文字列を渡すことができないうえに、HttpContext.Currentが存在していないためプロパティにアクセス時に例外発生するという問題が発生する。
ここで本題であるMicrosoft Fakesを利用することで、本来存在しないはずのHttpContext.Current及びその先のプロパティを偽装することができるのである。

Fakesを用いたテストの実装

Fakesで偽装をしたい対象がHttpCotextクラスのCurrentプロパティなので、このクラスを含むSystem.Web.dllをFakesアセンブリに追加する
image.png

すると、Fakesというフォルダがテストプロジェクトに追加され、このdllに含まれるクラスのクラスを偽装できることがわかる。
image.png

次に、HttpContextを偽装していくわけだが、偽装の定義はShimContextのスコープ内でしか利用できないので、テストの最中だけShimContextを用いるようusing節の内部にテストコードを書くことになる。もちろんusing節を必ずしも用いる必要はないのだが、その場合自分でShimContextを破棄しなければならないので注意が必要である。

偽装の実装法は、ラムダ式で定義するのが一般的なため以下のようなコードになる。

defaultTests.cs
usingSystem;usingSystem.Collections;usingSystem.Collections.Generic;usingSystem.Collections.Specialized;usingSystem.Web;usingMicrosoft.QualityTools.Testing.Fakes;usingMicrosoft.VisualStudio.TestTools.UnitTesting;namespaceWebAppTest_Test{[TestClass]publicclassdefaultTests{[TestMethod]publicvoidPage_Load_Test(){using(ShimsContext.Create()){#region偽装の実装// Request.Formが返すダミーを準備NameValueCollectiondummyForm=newNameValueCollection();// Request.QueryStringが返すダミーを宣言NameValueCollectiondummyQS=newNameValueCollection();// Request.Itemsが返すダミーを準備NameValueCollectiondummyItems=newNameValueCollection();// HttpContext.Requestが返すダミーを準備varfakeRequest=newSystem.Web.Fakes.ShimHttpRequest(){// Formプロパティの偽装FormGet=()=>dummyForm,// QueryStringの偽装QueryStringGet=()=>dummyQS,ItemGetString=(stringkey)=>dummyItems[key]};// HttpContext.Sessionが返すダミーを準備DummySessionStatedummySessionItem=newDummySessionState();varfakeSessionState=newSystem.Web.SessionState.Fakes.ShimHttpSessionState(){ItemGetString=(x)=>dummySessionItem[x],ItemSetStringObject=(x,value)=>dummySessionItem[x]=value};// HttpContextのItemsが返すダミーを準備varfakeItems=newHashtable();// HttpContext.Curretnの偽装System.Web.Fakes.ShimHttpContext.CurrentGet=()=>newSystem.Web.Fakes.ShimHttpContext(){// HttpContext.Current.Requestの偽装RequestGet=()=>fakeRequest,// HttpContext.Current.Sessionの偽装SessionGet=()=>fakeSessionState,// // HttpContext.Current.Itemsの偽装ItemsGet=()=>fakeItems,};#endregion// 例外を発生させる文言をセットdummyItems["presentation"]="例外表示";varpage=newdefaultPageInheritor();Assert.ThrowsException<ApplicationException>(()=>page.Page_Load(null,null));}}}}

ここで、DummySessionStateクラスはSessionの偽装用クラスでどのように実装してもいいのだが、一例としては以下のような実装が考えられる。

DummySessionState.cs
usingSystem.Collections;usingSystem.Collections.Generic;namespaceWebAppTest_Test{classDummySessionState:IDictionary<string,object>{Dictionary<string,object>innerDic=newDictionary<string,object>();publicobjectthis[stringkey]{get{if(innerDic.ContainsKey(key)){returninnerDic[key];}else{returnnull;}}set=>((IDictionary<string,object>)innerDic)[key]=value;}publicICollection<string>Keys=>((IDictionary<string,object>)innerDic).Keys;publicICollection<object>Values=>((IDictionary<string,object>)innerDic).Values;publicintCount=>((ICollection<KeyValuePair<string,object>>)innerDic).Count;publicboolIsReadOnly=>((ICollection<KeyValuePair<string,object>>)innerDic).IsReadOnly;publicvoidAdd(stringkey,objectvalue){((IDictionary<string,object>)innerDic).Add(key,value);}publicvoidAdd(KeyValuePair<string,object>item){((ICollection<KeyValuePair<string,object>>)innerDic).Add(item);}publicvoidClear(){((ICollection<KeyValuePair<string,object>>)innerDic).Clear();}publicboolContains(KeyValuePair<string,object>item){return((ICollection<KeyValuePair<string,object>>)innerDic).Contains(item);}publicboolContainsKey(stringkey){return((IDictionary<string,object>)innerDic).ContainsKey(key);}publicvoidCopyTo(KeyValuePair<string,object>[]array,intarrayIndex){((ICollection<KeyValuePair<string,object>>)innerDic).CopyTo(array,arrayIndex);}publicIEnumerator<KeyValuePair<string,object>>GetEnumerator(){return((IEnumerable<KeyValuePair<string,object>>)innerDic).GetEnumerator();}publicboolRemove(stringkey){return((IDictionary<string,object>)innerDic).Remove(key);}publicboolRemove(KeyValuePair<string,object>item){return((ICollection<KeyValuePair<string,object>>)innerDic).Remove(item);}publicboolTryGetValue(stringkey,outobjectvalue){return((IDictionary<string,object>)innerDic).TryGetValue(key,outvalue);}IEnumeratorIEnumerable.GetEnumerator(){return((IEnumerable)innerDic).GetEnumerator();}}}

Visual Studioでは、インターフェース(今の場合IDictionary)の継承クラス内に、そのインターフェースを実装したクラスをフィールド(今の場合innerDic)として保持していると、そのフィールドを通じてクラス実装のコードを自動生成してくれる機能があるので、このような実装も簡単に行うことができる。

多少コードが長くなったが、内容はシンプルなので少し読んでみれば簡単にわかる内容となっているはずである。
この程度のコーディングでテストを可能にするFakesはテストの適用範囲を広げてくれる可能性を感じさせてくれるのではないだろうか。

まとめ

今回の例では、本来RequestのItemsのみを偽装すればテストには十分だったが、よくHttpContextからアクセスする可能性のあるFormやQueryStringなどのプロパティに関しても偽装を実装してみた。

Fakesを用いれば、今回のような静的プロパティを偽装してテストを実施できることを示したが、冒頭にも記したようにグローバルオブジェクトのように振舞えるHttpContextへのアクセスは限定的にし、テスト可能なクラス設計を行うことが正攻法なので、何でもかんでも偽装をすればよいとクラス構成をおろそかにしてはならないことに注意しよう。


Viewing all articles
Browse latest Browse all 9366

Latest Images

Trending Articles