2

我打算編寫一個ActionFilter進行業務驗證,其中一些服務將通過服務定位器解決(我知道這不是好的做法,並且儘可能避免Service Locator模式,但是對於這種情況我想使用它)。過濾器的如何在使用服務定位器時爲ActionFilter編寫單元測試

OnActionExecuting方法是這樣的:

public override void OnActionExecuting(ActionExecutingContext actionContext) 
    { 
     // get validator for input; 
     var validator = actionContext.HttpContext.RequestServices.GetService<IValidator<TypeOfInput>>();// i will ask another question for this line 
     if(!validator.IsValid(input)) 
     { 
      //send errors 
     } 
    } 

是否可以編寫單元測試上述ActionFilter又如何呢?

+2

歸結起來實例'ActionExecutingContext'自己,傳遞一個'ActionContext'用嘲笑'HttpContext'(它是抽象的,所以你可以嘲笑它),從這裏,返回嘲笑'IServi來自'RequestServices'的ceProvider'。確切的用法和示例取決於您的Mock框架 – Tseng

回答

7

這是一個關於如何創建模擬(使用XUnit和Moq框架)來驗證IsValid方法被調用以及模擬返回false的示例。

using Dealz.Common.Web.Tests.Utils; 
using Microsoft.AspNetCore.Http; 
using Microsoft.AspNetCore.Mvc.Filters; 
using Microsoft.Extensions.DependencyInjection; 
using Moq; 
using System; 
using Xunit; 

namespace Dealz.Common.Web.Tests.ActionFilters 
{ 
    public class TestActionFilter 
    { 
     [Fact] 
     public void ActionFilterTest() 
     { 
      /**************** 
      * Setup 
      ****************/ 

      // Create the userValidatorMock 
      var userValidatorMock = new Mock<IValidator<User>>(); 
      userValidatorMock.Setup(validator => validator 
       // For any parameter passed to IsValid 
       .IsValid(It.IsAny<User>()) 
      ) 
      // return false when IsValid is called 
      .Returns(false) 
      // Make sure that `IsValid` is being called at least once or throw error 
      .Verifiable(); 

      // If provider.GetService(typeof(IValidator<User>)) gets called, 
      // IValidator<User> mock will be returned 
      var serviceProviderMock = new Mock<IServiceProvider>(); 
      serviceProviderMock.Setup(provider => provider.GetService(typeof(IValidator<User>))) 
       .Returns(userValidatorMock.Object); 

      // Mock the HttpContext to return a mockable 
      var httpContextMock = new Mock<HttpContext>(); 
      httpContextMock.SetupGet(context => context.RequestServices) 
       .Returns(serviceProviderMock.Object); 


      var actionExecutingContext = HttpContextUtils.MockedActionExecutingContext(httpContextMock.Object, null); 

      /**************** 
      * Act 
      ****************/ 
      var userValidator = new ValidationActionFilter<User>(); 
      userValidator.OnActionExecuting(actionExecutingContext); 

      /**************** 
      * Verify 
      ****************/ 

      // Make sure that IsValid is being called at least once, otherwise this throws an exception. This is a behavior test 
      userValidatorMock.Verify(); 

      // TODO: Also Mock HttpContext.Response and return in it's Body proeprty a memory stream where 
      // your ActionFilter writes to and validate the input is what you desire. 
     } 
    } 

    class User 
    { 
     public string Username { get; set; } 
    } 

    class ValidationActionFilter<T> : IActionFilter where T : class, new() 
    { 
     public void OnActionExecuted(ActionExecutedContext context) 
     { 
      throw new NotImplementedException(); 
     } 

     public void OnActionExecuting(ActionExecutingContext actionContext) 
     { 
      var type = typeof(IValidator<>).MakeGenericType(typeof(T)); 

      var validator = (IValidator<T>)actionContext.HttpContext 
       .RequestServices.GetService<IValidator<T>>(); 

      // Get your input somehow 
      T input = new T(); 

      if (!validator.IsValid(input)) 
      { 
       //send errors 
       actionContext.HttpContext.Response.WriteAsync("Error"); 
      } 
     } 
    } 

    internal interface IValidator<T> 
    { 
     bool IsValid(T input); 
    } 
} 

HttpContextUtils.cs

using Microsoft.AspNetCore.Http; 
using Microsoft.AspNetCore.Mvc; 
using Microsoft.AspNetCore.Mvc.Filters; 
using System.Collections.Generic; 

namespace Dealz.Common.Web.Tests.Utils 
{ 
    public class HttpContextUtils 
    { 
     public static ActionExecutingContext MockedActionExecutingContext(
      HttpContext context, 
      IList<IFilterMetadata> filters, 
      IDictionary<string, object> actionArguments, 
      object controller 
     ) 
     { 
      var actionContext = new ActionContext() { HttpContext = context }; 

      return new ActionExecutingContext(actionContext, filters, actionArguments, controller); 
     } 
     public static ActionExecutingContext MockedActionExecutingContext(
      HttpContext context, 
      object controller 
     ) 
     { 
      return MockedActionExecutingContext(context, new List<IFilterMetadata>(), new Dictionary<string, object>(), controller); 
     } 
    } 
} 

正如你可以看到,這是一個相當混亂,你需要創建大量的嘲笑的模擬實際工作類不同的反應,才能夠單獨測試ActionAttribute。

+0

謝謝。您還回答了我的下一個問題(通用操作篩選器)。 –

+0

如果你的屬性也是通用的,那麼這個方法就行得通。如果不是,你可以在運行時產生泛型:'var type = typeof(IValidator <>)。MakeGenericType(inputType);''其中'inputType'可以是'Type inputType = typeof(User)'或字符串'類型inputType = Type.GetType(「System.String」);' – Tseng

+0

@Tseng當Moq嘗試構造HttpContext時,由於routeData爲null,導致ArgumentNullException。我正在使用asp.net核心1.1我應該怎麼做? – LxL

4

我喜歡@ Tseng的上面的答案,但想到給予更多的答案,因爲他的答案涵蓋了更多的場景(比如泛型),並且可能對某些用戶來說是壓倒性的。

這裏我有一個動作過濾器屬性,它通過在上下文中設置Result屬性來檢查ModelState和短路(返回沒有被調用動作的響應)請求。在過濾器中,我嘗試使用的ServiceLocator模式獲得記錄器記錄一些數據(有些人可能不喜歡這樣,但這是一個例子)

過濾

public class ValidationFilterAttribute : ActionFilterAttribute 
{ 
    public override void OnActionExecuting(ActionExecutingContext context) 
    { 
     if (!context.ModelState.IsValid) 
     { 
      var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ValidationFilterAttribute>>(); 
      logger.LogWarning("some message here"); 

      context.Result = new JsonResult(new InvalidData() { Message = "some messgae here" }) 
      { 
       StatusCode = 400 
      }; 
     } 
    } 
} 

public class InvalidData 
{ 
    public string Message { get; set; } 
} 

單元測試

[Fact] 
public void ValidationFilterAttributeTest_ModelStateErrors_ResultInBadRequestResult() 
{ 
    // Arrange 
    var serviceProviderMock = new Mock<IServiceProvider>(); 
    serviceProviderMock 
     .Setup(serviceProvider => serviceProvider.GetService(typeof(ILogger<ValidationFilterAttribute>))) 
     .Returns(Mock.Of<ILogger<ValidationFilterAttribute>>()); 
    var httpContext = new DefaultHttpContext(); 
    httpContext.RequestServices = serviceProviderMock.Object; 
    var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); 
    var actionExecutingContext = new ActionExecutingContext(
     actionContext, 
     filters: new List<IFilterMetadata>(), // for majority of scenarios you need not worry about populating this parameter 
     actionArguments: new Dictionary<string, object>(), // if the filter uses this data, add some data to this dictionary 
     controller: null); // since the filter being tested here does not use the data from this parameter, just provide null 
    var validationFilter = new ValidationFilterAttribute(); 

    // Act 
    // Add an erorr into model state on purpose to make it invalid 
    actionContext.ModelState.AddModelError("Age", "Age cannot be below 18 years."); 
    validationFilter.OnActionExecuting(actionExecutingContext); 

    // Assert 
    var jsonResult = Assert.IsType<JsonResult>(actionExecutingContext.Result); 
    Assert.Equal(400, jsonResult.StatusCode); 
    var invalidData = Assert.IsType<InvalidData>(jsonResult.Value); 
    Assert.Equal("some messgae here", invalidData.Message); 
} 
相關問題