ASP.NET Web APIのSelf-Hostでテスト
これはOne ASP.NET Advent Calendar 2012の18日目の記事です。
昨日は、karuakunさんでASP.NET 4 WebFormでモバイルサイトを作成する場合のスクリプト結合でした。
はじめに
ASP.NET Web APIにはSelf-Hostと呼ばれる機能があります。IISがなくても動くということですね。この機能の使いどころは何だろうか?と考えたときに、一番しっくりしたのがテストでの利用でした。というわけで、Self-Hostをテストで使う方法を紹介したいと思います。テストのツールにはMSTestを使いますが、NUnitなどasync/awaitに対応したツールならほとんど同じ方法が使えると思います。
プロジェクトの構成
まず、プロジェクトの構成はこんな感じです。WebApiがControllerを含むテスト対象プロジェクト、WebApiTestがテストコードを含むプロジェクトです。
それぞれのプロジェクトに含まれるソースコードを示します。
WebApiプロジェクト
データをあらわすPersonクラス。
namespace WebApi { public class Person { public string Name { get; set; } public int Age { get; set; } } }
リクエストを受けてレスポンスを返すPersonController。
using System.Collections.Generic; using System.Web.Http; namespace WebApi { public class PersonController : ApiController { public IEnumerable<Person> Get() { return new List<Person> { new Person{ Name = "hoge", Age = 10 }, new Person{ Name = "foo", Age = 20 } }; } } }
WebApiTestプロジェクト
PersonControllerをTestするクラス。
using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Dispatcher; using System.Web.Http.SelfHost; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using WebApi; namespace WebApiTest { // このテストを動かすにはVisual Stuido を管理者権限で起動するか、Netsh.exeで設定必要。 // http://www.asp.net/web-api/overview/hosting-aspnet-web-api/self-host-a-web-api [TestClass] public class PersonControllerTest { private static readonly Uri BaseAddress = new Uri("http://localhost:9090/"); private readonly HttpSelfHostConfiguration _config = new HttpSelfHostConfiguration(BaseAddress); [TestInitialize] public void Initialize() { _config.Routes.MapHttpRoute( name: "Default", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional }); // エラーメッセージをレスポンスで返す _config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; // SelfHostのデフォルトのIAssembliesResolverはエントリポイントになった // アセンブリの中からコントローラーを探すがテストでは不適切。 // コントローラーを含むアセンブリから探す実装で置き換える _config.Services.Replace(typeof(IAssembliesResolver), new AssembliesResolver(typeof(PersonController).Assembly)); // POCOのプロパティ名の先頭を小文字にしてJSONのキーとする var jsonSettings = _config.Formatters.JsonFormatter.SerializerSettings; jsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } // asyncをつける場合、戻り値をTaskにしないとMSTestがテストメソッドだと認識しないので注意 [TestMethod] public async Task TestGet() { using (var server = new HttpSelfHostServer(_config)) { await server.OpenAsync(); using (var client = new HttpClient { BaseAddress = BaseAddress }) using (var response = await client.GetAsync("api/person")) { var content = await response.Content.ReadAsStringAsync(); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, content); var result = JArray.Parse(content); Assert.AreEqual(2, result.Count); var person = result[0]; Assert.AreEqual("hoge", person["name"]); Assert.AreEqual(10, person["age"]); person = result[1]; Assert.AreEqual("foo", person["name"]); Assert.AreEqual(20, person["age"]); } } } } }
Controllerを含むアセンブリを解決するクラス。
using System.Collections.Generic; using System.Reflection; using System.Web.Http.Dispatcher; namespace WebApiTest { public class AssembliesResolver : IAssembliesResolver { private readonly Assembly _assembly; public AssembliesResolver(Assembly assembly) { _assembly = assembly; } public ICollection<Assembly> GetAssemblies() { return new[] { _assembly }; } } }
解説
いくつかはコメントに書いていますが、次のようなポイントがあります。
- 指定のportでリッスンできるように、Visual Stuidoを管理者権限で起動するか、Netsh.exeを使う必要があります
- 他のプロジェクトからControllerを見つけられるようにIAssembliesResolverの実装を作る必要があります
- WebApiのアセンブリはSelf-Hostなプロジェクトから独立しています。Web-Hostなプロジェクトからも利用できます、Web-Hostなプロジェクトを別途作れば。
テストメソッドではサーバを起動して、それからHttpClientでリクエスト飛ばしてレスポンス受け取って、結果が期待通りのJSONか検証しています。
参考
基本はドキュメントとソース。
ソースコードを見るとわかりますが、ASP.NET WebAPIはTaskを使いまくりです。Taskを使った処理とSynchronizationContextをうまく連携させたり、スレッドやSynchronizationContextの無駄なスイッチを避けるような仕組みも入っています。ここの解説が詳しいです。
SynchronizationContextは、ufcppさんの記事もとてもわかりやすいですね。
まとめ
Controllerを自分でnewしてテストすることもできますが、それはあくまでControllerの単体のテストです。Self-Hostを使うと、Controllerの呼び出し前後で動くフレームワーク部分を含めてテストができます。たとえば、上で示したコードではJSONのシリアライズ方法をカスタマイズしていますが、そういう部分が意図通りに動いているか確かめることができます。ASP.NET Web APIは、機能や拡張ポイントが豊富だったりTaskを多用していたりして、何ができてどう動くかわかりづらい場合があります。でも、心配なし。そんなときは、Self-Hostを使ったテストで試行錯誤してみればいいんです。
明日は、masa_edwさんです。