2

Ich plane eine ActionFilter für Business-Validierung zu schreiben und in dem einige Dienste über Service Locator gelöst werden (Ich weiß, das ist nicht gute Praxis und soweit wie möglich vermeide ich Service Locator Pattern, aber für diesen Fall möchte ich es verwenden).Wie Unit Test für ActionFilter schreiben mit Service Locator

OnActionExecuting Methode des Filters ist so etwas wie diese:

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 
     } 
    } 

Ist es möglich, Unit-Test für oben ActionFilter und wie zu schreiben?

+2

zu instanziieren läuft darauf hinaus, 'ActionExecutingContext' selbst, ein' ActionContext' mit einem verspottet 'HttpContext' (es ist abstrakt, so können Sie es verspotten) vorbei und von hier aus, kehren ein verspottet' IServi ceProvider 'aus' RequestServices'. Genaue Verwendung und Beispiele hängen von Ihrem Mock Framework ab – Tseng

Antwort

7

Hier ist ein Beispiel zum Erstellen eines Mocks (unter Verwendung von XUnit und Moq Framework), um zu verifizieren, dass die IsValid-Methode aufgerufen wird und der Mock eine false zurückgibt.

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); 
     } 
    } 
} 

Wie Sie sehen, es ist eine ziemliche Sauerei, müssen Sie viel Mocks schaffen unterschiedliche Reaktionen der actuall Klassen zu simulieren, nur um in der Lage sein Testen Sie das ActionAttribute isoliert.

+0

Danke. Du hast auch meine nächste Frage beantwortet (Generic Action Filter). –

+0

Das funktioniert, wenn Ihr Attribut auch generisch ist. Falls nicht, können Sie Generics auch während der Laufzeit generieren: 'var type = typeof (IValidator <>) .MakeGenericType (inputType);' where 'inputType' könnte zB' Type inputType = typeof (User) 'oder from string' sein Type inputType = Type.GetType ("System.String"); ' – Tseng

+0

@Tseng Ich habe ArgumentNullException aufgrund der routeData ist null, wenn Moq versucht, HttpContext zu konstruieren. Ich benutze asp.net Core 1.1 Was soll ich tun? – LxL

4

Ich mag die obige Antwort von @Tseng, dachte aber darüber nach, noch eine Antwort zu geben, da seine Antwort weitere Szenarien (wie Generika) abdeckt und für einige Benutzer überwältigend sein könnte.

Hier habe ich ein Aktionsfilter-Attribut, das nur die ModelState überprüft und die Anforderung kurzschließt (gibt die Antwort zurück, ohne dass die Aktion aufgerufen wird), indem die Eigenschaft Result für den Kontext festgelegt wird. Innerhalb des Filters, versuche ich das Servicelocator Muster zu verwenden, um einen Logger zu bekommen einige Daten einzuloggen (einige vielleicht nicht mögen, aber dies ist ein Beispiel)

Filter

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; } 
} 

Einheit Test

[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); 
}