使用ControllerActionInvoker进行单元测试以使用参数调用操作

时间:2009-07-17 19:43:12

标签: asp.net-mvc unit-testing controlleractioninvoker

我使用ControllerActionInvoker从单元测试中调用控制器操作

var controllerInvoker = new ControllerActionInvoker();
var result = controllerInvoker.InvokeAction(
                 testController.ControllerContext, "Default" );

如何使用它来调用具有参数的动作?

[AcceptVerbs( HttpVerbs.Post )]
[ActionException( SomeAttribute )]
public SomeResult AddMethod( long[] Ids )
{
    //some code
}

3 个答案:

答案 0 :(得分:1)

从文档中看,您希望使用InvokeActionMethod方法,该方法允许您将IDictionary中的参数作为第三个参数传递。

ControllerContext实际上带有控制器将用于绑定的附加数据(过滤器,模型绑定器,路由数据)。您的参数需要通过ControllerContext传递。

我找到了example about unit testing controllers

答案 1 :(得分:0)

您不应在单元测试中使用ControllerActionInvoker。你到底想要完成什么?

如果您正在尝试测试行为的行为,请直接调用它们(它们只是常规方法)。如果您正在尝试测试过滤器的行为,请为过滤器创建模拟上下文并调用其OnXxx()方法。

答案 2 :(得分:0)

我之所以使用ControllerActionInvoker,是因为我想围绕控制器编写规格测试,而不是底层单元测试。我发现,我对ControllerActionInvoker的实现必须根据我正在测试的内容进行改进,但是对于以下情况而言,对我来说是有效的。

class ControllerSpecActionInvoker<TResult> : ControllerActionInvoker where 
TResult : ActionResult
{
  private readonly Expression body;

  public ControllerSpecActionInvoker(Expression body)
  {
    this.body = body;
  }

  public TResult Result { get; private set; }

  protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
    => Result = actionResult as TResult;

  protected override IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
  {
    if (body is MethodCallExpression methodCall)
    {
      return methodCall.Method.GetParameters()
        .Zip(methodCall.Arguments.Select(GetArgumentAsConstant), (param, arg) => new { param.Name, Value = ChangeType(arg.Value, param.ParameterType) })
          .ToDictionary(item => item.Name, item => item.Value);
    }
    return base.GetParameterValues(controllerContext, actionDescriptor);
  }

  private ConstantExpression GetArgumentAsConstant(Expression exp)
  {
    switch (exp)
    {
      case ConstantExpression constExp:
        return constExp;
      case UnaryExpression uranExp:
        return GetArgumentAsConstant(uranExp.Operand);
    }

    throw new NotSupportedException($"Cannot handle expression of type '{exp.GetType()}'");
  }

  private static object ChangeType(object value, Type conversion)
  {
    var t = conversion;

    if (!t.IsGenericType || t.GetGenericTypeDefinition() != typeof(Nullable<>)) return Convert.ChangeType(value, t);

    if (value == null) return null;

    t = Nullable.GetUnderlyingType(t);

    return Convert.ChangeType(value, t);
  }
}

出于我的目的,这用于规范夹具的基类和自动模拟依赖项中,但使用方式的本质是这样的:

var actionInvoker = new ControllerSpecActionInvoker<ActionResult>(Expression<Func<ActionResult|JsonResult|Etc>>);
actionInvoker.InvokeAction(<controller context>, <name of the action>);
Result = actionInvoker.Result;

因此它可能看起来像未经测试的东西。大部分杂物都可以隐藏在基类中:

class MyController : Controller
{
  JsonResult MyAction(int i) { return Json(new {}); } 
}

class MyControllerFixture
{
  [Test]
  public void ReturnsData()
  {
    var controller = new MyController();
    var controllerContext = new ControllerContext
    {
      RouteData = new RouteData(),
      HttpContext = httpContextBase,
    };
    controllerContext.Controller = controller;
    controller.ControllerContext = controllerContext;
    Action<JsonResult> act = controller.MyAction(1);
    var actionInvoker = new ControllerSpecActionInvoker<JsonResult>(act.Body);
    actionInvoiker.Result.Should().NotBeNull();
  }
}