39

WebApiルート構成の単体テストを実行しようとしています。ルートが私のメソッドに"/api/super"マップされていることをテストしたいと思います。以下のテストを設定しましたが、いくつか問題があります。Get()SuperController

public void GetTest()
{
    var url = "~/api/super";

    var routeCollection = new HttpRouteCollection();
    routeCollection.MapHttpRoute("DefaultApi", "api/{controller}/");

    var httpConfig = new HttpConfiguration(routeCollection);
    var request = new HttpRequestMessage(HttpMethod.Get, url);

    // exception when url = "/api/super"
    // can get around w/ setting url = "http://localhost/api/super"
    var routeData = httpConfig.Routes.GetRouteData(request);
    request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;

    var controllerSelector = new DefaultHttpControllerSelector(httpConfig);

    var controlleDescriptor = controllerSelector.SelectController(request);

    var controllerContext =
        new HttpControllerContext(httpConfig, routeData, request);
    controllerContext.ControllerDescriptor = controlleDescriptor;

    var selector = new ApiControllerActionSelector();
    var actionDescriptor = selector.SelectAction(controllerContext);

    Assert.AreEqual(typeof(SuperController),
        controlleDescriptor.ControllerType);
    Assert.IsTrue(actionDescriptor.ActionName == "Get");
}

私の最初の問題は、完全修飾URLを指定しないと、「この操作は相対URIではサポートされていません」というメッセージとともに例外がhttpConfig.Routes.GetRouteData(request);スローされることです。InvalidOperationException

スタブ構成で明らかに何かが欠けています。ルートテストに完全修飾URIを使用するのは合理的ではないように思われるため、相対URIを使用することをお勧めします。

上記の構成に関する2番目の問題は、RouteConfigで構成されたルートをテストしておらず、代わりに次のものを使用していることです。

var routeCollection = new HttpRouteCollection();
routeCollection.MapHttpRoute("DefaultApi", "api/{controller}/");

RouteTable.Routes一般的なGlobal.asaxで構成されているように割り当てられたものを利用するにはどうすればよいですか。

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        // other startup stuff

        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        // route configuration
    }
}

さらに、上記でスタブアウトしたものは、最良のテスト構成ではない可能性があります。より合理化されたアプローチがあれば、私はすべての耳です。

4

7 に答える 7

25

私は最近、Web API ルートをテストしていましたが、これがその方法です。

  1. 最初に、すべての Web API ルーティング ロジックをそこに移動するヘルパーを作成しました。
    public static class WebApi
    {
        public static RouteInfo RouteRequest(HttpConfiguration config, HttpRequestMessage request)
        {
            // create context
            var controllerContext = new HttpControllerContext(config, Substitute.For<IHttpRouteData>(), request);

            // get route data
            var routeData = config.Routes.GetRouteData(request);
            RemoveOptionalRoutingParameters(routeData.Values);

            request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
            controllerContext.RouteData = routeData;

            // get controller type
            var controllerDescriptor = new DefaultHttpControllerSelector(config).SelectController(request);
            controllerContext.ControllerDescriptor = controllerDescriptor;

            // get action name
            var actionMapping = new ApiControllerActionSelector().SelectAction(controllerContext);

            return new RouteInfo
            {
                Controller = controllerDescriptor.ControllerType,
                Action = actionMapping.ActionName
            };
        }

        private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValues)
        {
            var optionalParams = routeValues
                .Where(x => x.Value == RouteParameter.Optional)
                .Select(x => x.Key)
                .ToList();

            foreach (var key in optionalParams)
            {
                routeValues.Remove(key);
            }
        }
    }

    public class RouteInfo
    {
        public Type Controller { get; set; }

        public string Action { get; set; }
    }
  1. Web API ルートを登録する別のクラスがあると仮定します (既定では、Visual Studio ASP.NET MVC 4 Web アプリケーション プロジェクトの App_Start フォルダーに作成されます)。
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
  1. ルートを簡単にテストできます。
    [Test]
    public void GET_api_products_by_id_Should_route_to_ProductsController_Get_method()
    {
        // setups
        var request = new HttpRequestMessage(HttpMethod.Get, "http://myshop.com/api/products/1");
        var config = new HttpConfiguration();

        // act
        WebApiConfig.Register(config);
        var route = WebApi.RouteRequest(config, request);

        // asserts
        route.Controller.Should().Be<ProductsController>();
        route.Action.Should().Be("Get");
    }

    [Test]
    public void GET_api_products_Should_route_to_ProductsController_GetAll_method()
    {
        // setups
        var request = new HttpRequestMessage(HttpMethod.Get, "http://myshop.com/api/products");
        var config = new HttpConfiguration();

        // act
        WebApiConfig.Register(config);
        var route = WebApi.RouteRequest(config, request);

        // asserts
        route.Controller.Should().Be<ProductsController>();
        route.Action.Should().Be("GetAll");
    }

    ....

以下のいくつかのメモ:

  • はい、絶対 URL を使用しています。しかし、これらは偽の URL であり、機能するために何も構成する必要がなく、Web サービスへの実際の要求を表しているため、ここでは問題は見られません。
  • ルーティング マッピング コードをテストにコピーする必要はありません (上記の例のように) HttpConfiguration 依存関係を持つ別のクラスで構成されている場合。
  • 上記の例では NUnit、NSubstitute、および FluentAssertions を使用していますが、もちろん、他のテスト フレームワークでも同じことを行うのは簡単な作業です。
于 2013-01-17T01:04:34.003 に答える
9

この回答は WebAPI 2.0 以降で有効です

Whyleee の回答を読んで、アプローチが結合された脆弱な仮定に基づいていることに気付きました。

  1. このアプローチでは、アクションの選択を再作成しようとし、Web API での内部実装の詳細を想定しています。
  2. 既定のコントローラー セレクターが使用されていることを前提としています。これは、それを置き換えることができる既知の公開拡張ポイントがある場合です。

別のアプローチは、軽量の機能テストを使用することです。このアプローチの手順は次のとおりです。

  1. WebApiConfig.Register メソッドを使用してテスト用の HttpConfiguration オブジェクトを初期化し、現実世界でアプリが初期化される方法を模倣します。
  2. そのレベルでアクション情報を取得するカスタム認証フィルターをテスト構成オブジェクトに追加します。これは、スイッチを介して製品コードに直接挿入または実行できます。2.1 認証フィルターはアクション コードと同様にフィルターを短絡するため、アクション メソッド自体で実行されている実際のコードに問題はありません。
  3. インメモリサーバー(HttpServer)を利用してリクエストを行います。このアプローチはメモリ内チャネルを使用するため、ネットワークにヒットしません。
  4. 取得したアクション情報を期待される情報と比較します。
[TestClass]
public class ValuesControllerTest
{
    [TestMethod]
    public void ActionSelection()
    {
        var config = new HttpConfiguration();
        WebApiConfig.Register(config);

        Assert.IsTrue(ActionSelectorValidator.IsActionSelected(
            HttpMethod.Post,
            "http://localhost/api/values/",
            config,
            typeof(ValuesController),
            "Post"));
    }
 }

このヘルパーはパイプラインを実行し、認証フィルターによってキャプチャされたデータを検証します。他のプロパティもキャプチャできます。または、初期化時にフィルターにラムダを渡すことで、テストごとに直接検証を行う顧客フィルターを実装できます。

 public class ActionSelectorValidator
 {
    public static bool IsActionSelected(
        HttpMethod method,
        string uri,
        HttpConfiguration config,
        Type controller,
        string actionName)
    {
        config.Filters.Add(new SelectedActionFilter());
        var server = new HttpServer(config);
        var client = new HttpClient(server);
        var request = new HttpRequestMessage(method, uri);
        var response = client.SendAsync(request).Result;
        var actionDescriptor = (HttpActionDescriptor)response.RequestMessage.Properties["selected_action"];

        return controller == actionDescriptor.ControllerDescriptor.ControllerType && actionName == actionDescriptor.ActionName;
    }
}

このフィルターが実行され、他のすべてのフィルターまたはアクション コードの実行がブロックされます。

public class SelectedActionFilter : IAuthenticationFilter
{
    public Task AuthenticateAsync(
         HttpAuthenticationContext context,
         CancellationToken cancellationToken)
    {
        context.ErrorResult = CreateResult(context.ActionContext);

       // short circuit the rest of the authentication filters
        return Task.FromResult(0);
    }

    public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
    {
        var actionContext = context.ActionContext;

        actionContext.Request.Properties["selected_action"] = 
            actionContext.ActionDescriptor;
        context.Result = CreateResult(actionContext); 


        return Task.FromResult(0);
    }

    private static IHttpActionResult CreateResult(
        HttpActionContext actionContext)
    {
        var response = new HttpResponseMessage()
            { RequestMessage = actionContext.Request };

        actionContext.Response = response;

        return new ByPassActionResult(response);
    }

    public bool AllowMultiple { get { return true; } }
}

実行をショートさせる結果

internal class ByPassActionResult : IHttpActionResult
{
    public HttpResponseMessage Message { get; set; }

    public ByPassActionResult(HttpResponseMessage message)
    {
        Message = message;
    }

    public Task<HttpResponseMessage> 
       ExecuteAsync(CancellationToken cancellationToken)
    {
       return Task.FromResult<HttpResponseMessage>(Message);
    }
}
于 2013-09-16T22:16:19.993 に答える
3

上記の回答をしてくれたwhyleeeに感謝します!

これを、WebApiContrib.Testing ライブラリの構文的に気に入った要素のいくつかと組み合わせましたが、次のヘルパー クラスを生成するには機能しませんでした。

これにより、このような非常に軽量なテストを書くことができます...

[Test]
[Category("Auth Api Tests")]
public void TheAuthControllerAcceptsASingleItemGetRouteWithAHashString()
{
    "http://api.siansplan.com/auth/sjkfhiuehfkshjksdfh".ShouldMapTo<AuthController>("Get", "hash");
}

[Test]
[Category("Auth Api Tests")]
public void TheAuthControllerAcceptsAPost()
{
    "http://api.siansplan.com/auth".ShouldMapTo<AuthController>("Post", HttpMethod.Post);
}

また、必要に応じてパラメーターをテストできるように少し拡張しました (これは params 配列なので、好きなものをすべて追加でき、それらが存在することを確認するだけです)。これは、純粋に私の選択したフレームワークであるため、MOQにも適合しています...

using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Hosting;
using System.Web.Http.Routing;

namespace SiansPlan.Api.Tests.Helpers
{
    public static class RoutingTestHelper
    {
        /// <summary>
        /// Routes the request.
        /// </summary>
        /// <param name="config">The config.</param>
        /// <param name="request">The request.</param>
        /// <returns>Inbformation about the route.</returns>
        public static RouteInfo RouteRequest(HttpConfiguration config, HttpRequestMessage request)
        {
            // create context
            var controllerContext = new HttpControllerContext(config, new Mock<IHttpRouteData>().Object, request);

            // get route data
            var routeData = config.Routes.GetRouteData(request);
            RemoveOptionalRoutingParameters(routeData.Values);

            request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
            controllerContext.RouteData = routeData;

            // get controller type
            var controllerDescriptor = new DefaultHttpControllerSelector(config).SelectController(request);
            controllerContext.ControllerDescriptor = controllerDescriptor;

            // get action name
            var actionMapping = new ApiControllerActionSelector().SelectAction(controllerContext);

            var info = new RouteInfo(controllerDescriptor.ControllerType, actionMapping.ActionName);

            foreach (var param in actionMapping.GetParameters())
            {
                info.Parameters.Add(param.ParameterName);
            }

            return info;
        }

        #region | Extensions |

        /// <summary>
        /// Determines that a URL maps to a specified controller.
        /// </summary>
        /// <typeparam name="TController">The type of the controller.</typeparam>
        /// <param name="fullDummyUrl">The full dummy URL.</param>
        /// <param name="action">The action.</param>
        /// <param name="parameterNames">The parameter names.</param>
        /// <returns></returns>
        public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, params string[] parameterNames)
        {
            return ShouldMapTo<TController>(fullDummyUrl, action, HttpMethod.Get, parameterNames);
        }

        /// <summary>
        /// Determines that a URL maps to a specified controller.
        /// </summary>
        /// <typeparam name="TController">The type of the controller.</typeparam>
        /// <param name="fullDummyUrl">The full dummy URL.</param>
        /// <param name="action">The action.</param>
        /// <param name="httpMethod">The HTTP method.</param>
        /// <param name="parameterNames">The parameter names.</param>
        /// <returns></returns>
        /// <exception cref="System.Exception"></exception>
        public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, HttpMethod httpMethod, params string[] parameterNames)
        {
            var request = new HttpRequestMessage(httpMethod, fullDummyUrl);
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);

            var route = RouteRequest(config, request);

            var controllerName = typeof(TController).Name;
            if (route.Controller.Name != controllerName)
                throw new Exception(String.Format("The specified route '{0}' does not match the expected controller '{1}'", fullDummyUrl, controllerName));

            if (route.Action.ToLowerInvariant() != action.ToLowerInvariant())
                throw new Exception(String.Format("The specified route '{0}' does not match the expected action '{1}'", fullDummyUrl, action));

            if (parameterNames.Any())
            {
                if (route.Parameters.Count != parameterNames.Count())
                    throw new Exception(
                        String.Format(
                            "The specified route '{0}' does not have the expected number of parameters - expected '{1}' but was '{2}'",
                            fullDummyUrl, parameterNames.Count(), route.Parameters.Count));

                foreach (var param in parameterNames)
                {
                    if (!route.Parameters.Contains(param))
                        throw new Exception(
                            String.Format("The specified route '{0}' does not contain the expected parameter '{1}'",
                                          fullDummyUrl, param));
                }
            }

            return true;
        }

        #endregion

        #region | Private Methods |

        /// <summary>
        /// Removes the optional routing parameters.
        /// </summary>
        /// <param name="routeValues">The route values.</param>
        private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValues)
        {
            var optionalParams = routeValues
                .Where(x => x.Value == RouteParameter.Optional)
                .Select(x => x.Key)
                .ToList();

            foreach (var key in optionalParams)
            {
                routeValues.Remove(key);
            }
        }

        #endregion
    }

    /// <summary>
    /// Route information
    /// </summary>
    public class RouteInfo
    {
        #region | Construction |

        /// <summary>
        /// Initializes a new instance of the <see cref="RouteInfo"/> class.
        /// </summary>
        /// <param name="controller">The controller.</param>
        /// <param name="action">The action.</param>
        public RouteInfo(Type controller, string action)
        {
            Controller = controller;
            Action = action;
            Parameters = new List<string>();
        }

        #endregion

        public Type Controller { get; private set; }
        public string Action { get; private set; }
        public List<string> Parameters { get; private set; }
    }
}
于 2013-08-30T11:04:07.470 に答える
-1

この場合、ルート コレクションからルート データを取得するには、完全な URI を指定する必要があります ("http://localhost/api/super" を使用するだけです)。

RouteTable.Routes からのルートをテストするには、次のようなことができる可能性があります。

var httpConfig = GlobalConfiguration.Configuration;
httpConfig.Routes.MapHttpRoute("DefaultApi", "api/{controller}/");

内部で起こっていることは、GlobalConfiguration が RouteTable.Routes を httpConfig.Routes に適応させるということです。したがって、ルートを httpConfig.Routes に追加すると、実際には RouteTable.Routes に追加されます。ただし、これが機能するには、HostingEnvironment.ApplicationVirtualPath などの環境設定が設定されるように、ASP.NET 内でホストされる必要があります。

于 2012-08-09T01:25:26.737 に答える