actionfilter
你好,
这是我为bytes.com写的第二篇文章,这次我希望分享一些有关如何在ASP.NET MVC 2 Framework中测试控制器的自定义ActionFilterAttribute的知识。
从Ruby on Rails的背景开始,我已经使用框架一个月了,并且开发了一个特殊的ActionFilter,它使用定制的HTTP标头针对我们的数据库执行用户身份验证。
筛选器的概念是检查这些标头的存在,然后从这些标头中提取所需的信息以执行身份验证。 过滤器的代码看起来像这样:
using System;
using System.Text;
using System.Web.Mvc;
using TenForce.Execution.Framework;
using TenForce.Execution.Api2.Implementation;
namespace TenForce.Execution.Web.Filters
{
/// <summary>
/// This class defines a custom Authentication attribute that can be applied on controllers.
/// This results in authentication occuring on all actions that are beeing defined in the controller
/// who implements this filter.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthenticationFilter : ActionFilterAttribute
{
#region IAuthorizationFilter Members
/// <summary>
/// This function get's called by the Mvc framework prior to performing any actions on
/// the controller. The function will check if a call is authorized by the caller.
/// The function will extract the username and password from the HTTP headers send by
/// the caller and will validate these against the database to see if there is a valid
/// account for the user.
/// If the user can be found in the database, operations will resume, otherwise the action
/// is canceled.
/// </summary>
/// <param name="filterContext">The context for the filter.</param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Call the base operations first.
base.OnActionExecuting(filterContext);
// Surround the entire authentication process with a try-catch to prevent errors from
// breaking the code.
try
{
// Extract the custom authorization header from the HTTP headers.
string customAuthHeader = Encoding.UTF8.GetString(Convert.FromBase64String(filterContext.RequestContext.HttpContext.Request.Headers["TenForce-Auth"]));
// Split the header in the subcomponents.
string[] components = customAuthHeader.Split('|');
// Check if both components are present.
if (components.Length >= 2)
{
// This header consists of 2 parts, the username and password, seperate by a vertical pipe.
string username = components[0] ?? string.Empty;
string password = components[1] ?? string.Empty;
// Validate the user against the database.
if (Authenticator.Authenticate(username, password))
{
// The request is valid, so add the custom header to inform the request was
// authorized.
AllowRequest(filterContext);
return;
}
}
// If we reach this point, the authorization request is no longer valid.
CancelRequest(filterContext);
}
catch (Exception ex)
{
// Log the exception that has occured.
Logger.Log(GetType(), ex);
// Cancel the request, as we could not properly process it.
CancelRequest(filterContext);
}
}
#endregion
#region Private Methods
/// <summary>
/// Cancels the Athorization and adds the custom tenforce header to the response to
/// inform the caller that his call has been denied.
/// </summary>
/// <param name="authContext">The authorizationContxt that needs to be canceled.</param>
private static void CancelRequest(ActionExecutingContext authContext)
{
authContext.Result = new HttpUnauthorizedResult();
authContext.HttpContext.Response.Headers.Add(@"Custom Response Header", @"Denied value");
}
/// <summary>
/// Allows the Authorization and adds the custom tenforce header to the response to
/// inform the claler that his call has been allowed.
/// </summary>
/// <param name="authContext">The authorizationContext that needs to be allowed.</param>
private static void AllowRequest(ActionExecutingContext authContext)
{
authContext.Result = null;
authContext.HttpContext.Response.Headers.Add(@"Custom Response Header", @"Accepted Value");
}
#endregion
}
}
该代码说明了它的目的。
但是,因为我们使用TDD,所以我们需要一个UnitTest,它实际上可以模拟此过滤器的必需条件并提供正确的属性,而该过滤器不知道它正在使用模拟数据。
这个问题的答案:模拟框架。
对于以下代码,我们将依赖以下框架来正确支持代码:
-最小起订量4
-吉利奥
-我们自己的框架(处理数据库流量)
步骤:1构造模拟对象
正确测试上面显示的ActionFilter功能的第一步是构造将代表HttpContext对象的Mock对象。 在MVC框架中,此对象包含与特定HTTP请求或响应有关的所有信息。 但是,棘手的部分是,各种属性是只读的,如果不在其周围构造巨大的包装器,则不能直接设置它们。
因此,为了构造对象,响应和请求,我们使用来自Mock框架的以下调用:
HttpRequest = new Mock<HttpRequestBase>();
HttpResponse = new Mock<HttpResponseBase>();
HttpContext = new Mock<HttpContextBase>();
ActionContext = new Mock<ActionExecutingContext>();
Filter = new Web.Filters.AuthenticationFilter();
请注意,这些值存储在Test类的属性中。
这些对象本身并没有做什么用。 我们需要告诉这些Mock对象如何响应来自Filter的请求,否则我们将被空引用异常轰炸。
当您查看过滤器的代码时,就可以证明我们需要访问以下部分:
-HttpContext的Response和Request属性
-Response对象的Headers属性
-Request对象的Headers属性
-用于请求身份验证的网址
因为每个对象都是全局ActionContext对象的子对象,所以我们需要逐步构建请求树。 以下代码演示了如何将对象链接在一起:
ActionContext.SetupGet(c => c.HttpContext).Returns(HttpContext.Object);
HttpContext.SetupGet(r => r.Request).Returns(HttpRequest.Object);
HttpContext.SetupGet(r => r.Response).Returns(HttpResponse.Object);
HttpResponse.SetupGet(x => x.Headers).Returns(new System.Net.WebHeaderCollection());
HttpRequest.SetupGet(r => r.RawUrl).Returns(@"http://test.yourdomain.com");
现在,我们已经将Mock对象配置为彼此了解并返回在调用Headers,Request或Response属性时可以使用的东西。
步骤2:执行单元测试
现在我们需要实际调用测试。 因为过滤器是一个类,所以我们可以从中创建一个实例,并使用UnitTest框架(例如NUnit)运行代码:
[Test]
public void SuccessfullAuthentication()
{
// Configure the Request and Response headers before making the call
// to the ActionFilter. Ensure the authentication header is present.
HttpRequest.SetupGet(r => r.Headers).Returns(new System.Net.WebHeaderCollection
{{@"YourHeader", "Header value"}});
// Call the action on the filter and check the response.
Filter.OnActionExecuting(ActionContext.Object);
// Check the ActionResult to null and that the response header contains the correct value.
Assert.IsTrue(ActionContext.Object.Result == null);
Assert.IsTrue(ActionContext.Object.HttpContext.Response.Headers["Response Header"].Equals(@"Response Header Value"));
}
该代码的作用是调用当接收到需要身份验证的请求时MVC框架将调用的函数。
在第一行代码中,我们模仿HTTP请求的调用,并使用正确的值插入ActionFilter所需的自定义标头。
第二行代码调用实际的过滤器代码,并执行整个验证过程,这可以在第一个代码段中看到。
最困难的部分是正确配置Mock框架,以模仿MVC调用。 因为我自己为此苦苦挣扎,而且解决方案太简单了,所以我决定与所有人分享这一见解。
翻译自: https://bytes.com/topic/asp-net/insights/905132-testing-actionfilter-mvc-controller
actionfilter