Unit Testing with ControllerActionInvoker to invoke action with parameters - asp.net-mvc

I use ControllerActionInvoker to invoke controller actions form unit tests
var controllerInvoker = new ControllerActionInvoker();
var result = controllerInvoker.InvokeAction(
testController.ControllerContext, "Default" );
How do I use it to call an action that has parameters?
[AcceptVerbs( HttpVerbs.Post )]
[ActionException( SomeAttribute )]
public SomeResult AddMethod( long[] Ids )
{
//some code
}

From the documentation it looks like you want to use the InvokeActionMethod method which allows you to pass parameters in an IDictionary as the third argument.
The ControllerContext actually carries with it additional data that the controller will use for binding (filters, model binders, route data). Your argument will need to be passed through the ControllerContext.
I found an example about unit testing controllers.

You shouldn't use the ControllerActionInvoker from within your unit tests. What are you actually trying to accomplish?
If you're trying to test the behavior of your actions, just call them directly (they are just regular methods). If you're trying to test the behavior of your filters, create a mock context for the filter and call its OnXxx() method.

I use the ControllerActionInvoker because I want to write specification tests around my controllers rather than low level unit tests. What I have found is that my implementation of the ControllerActionInvoker has had to evolve based on what I am testing but so for the following has worked for me.
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);
}
}
For my purposes this is used in specification fixture base class and auto mock dependencies but the essence of how you use it is like this:
var actionInvoker = new ControllerSpecActionInvoker<ActionResult>(Expression<Func<ActionResult|JsonResult|Etc>>);
actionInvoker.InvokeAction(<controller context>, <name of the action>);
Result = actionInvoker.Result;
So it might look something like this which is not test. Most of the cruft can be hidden away in a base class:
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();
}
}

Related

Unit test for customer ActionFilterAttribute failed

The project is aspnet core 2.1 and i use customer ActionFilterAttribute as belows:
public override void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
foreach (var parameter in descriptor.MethodInfo.GetParameters())
{
object args = null;
if (context.ActionArguments.ContainsKey(parameter.Name))
{
args = context.ActionArguments[parameter.Name];
}
ValidateAttributes(parameter, args, context.ModelState);
}
}
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
and now I add a unit test code to test the filter,
[TestMethod]
public void ValidateModelAttributes_SetsResultToBadRequest_IfModelIsInvalid()
{
var validationFilter = new ValidateModelStateAttribute();
var modelState = new ModelStateDictionary();
modelState.AddModelError("name", "invalid");
var actionDescriptor = new ActionDescriptor();
var actionContext = new ActionContext(
Mock.Of<HttpContext>(),
Mock.Of<RouteData>(),
Mock.Of<ActionDescriptor>(),
modelState
);
var actionExecutingContext = new ActionExecutingContext(
actionContext,
new List<IFilterMetadata>(),
new Dictionary<string, object>() { { "1", "1" } },
Mock.Of<Controller>()
);
validationFilter.OnActionExecuting(actionExecutingContext);
Assert.IsNotNull(actionExecutingContext.Result);
}
but the descriptor always return null:
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
Can you help me to fix the unit test code?
but the descriptor always return null`,
that is because in the test a Mock.Of<ActionDescriptor>() which will be a ActionDescriptor is used, but the subject under test casts to ControllerActionDescriptor, which the mock is not, and thus null.
Use the appropriate mock
//...omitted for brevity
var actionContext = new ActionContext(
Mock.Of<HttpContext>(),
Mock.Of<RouteData>(),
Mock.Of<ControllerActionDescriptor>(),
modelState
);
//...omitted for brevity
You would still need to setup the mock so that the accessed members behave as expected when exercising the test

Type-directed serialization in ValueProvider

I was hoping to encapsulate all the serialization logic of my json api using the ValueProvider facility of MS.NET MVC.
However, since my deserialization is type directed (i.e. it needs to know what class to return from deserializing), and I can simply find no reasonable way to get to the type that my ValueProvider is supposed to return.
Suppose we have a controller:
public class MyController : Controller
{
// We want obj to be instantiated by MyValueProvider
public ActionResult MyAction(MyComplexObject obj)
{
}
}
What I would then like to be able to write:
public class MyValueProvider : IValueProvider
{
public bool ContainsPrefix(string prefix)
{
...
}
public ValueProviderResult GetValue(string key)
{
switch (req.HttpMethod)
{
case "POST":
{
req.InputStream.Position = 0;
T retObj = MyDeserialize<T>(req.InputStream);
return new ValueProviderResult(retObj, key, CultureInfo.CurrentCulture);
}
break;
default:
return null;
}
}
}
(Though I realise that without actually binding T in MyValueProvider I would have to use reflection to invoke MyDeserialize with a runtime-provided Type)
I'm almost considering to just derive the runtime type to deserialize using some variable naming scheme keyed by the 'key' argument to GetValue.
After spending half day pondering this, and then reading http://ishwor.cyberbudsonline.com/2012/07/fun-with-aspnet-mvc-3-custom-json-model-binder.html, I have come to the conclusion that what I need is in fact a ModelBinder, as the ModelType is available in the BindingContext.
So, the ValueProvider in the question becomes:
public class MyModelBinder<T> : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
JToken resTok;
var key = bindingContext.ModelName;
var req = controllerContext.HttpContext.Request;
if (bindingContext.ModelType != typeof(T)) return null;
switch (req.HttpMethod)
{
case "POST":
req.InputStream.Position = 0;
T retObj = MyDeserialize<T>(req.InputStream);
return retObj;
break;
default:
return null;
}
}
}

MVC pass ids separated by "+" to action

I want to have possibility to access action by the following URL type:
http://localhost/MyControllerName/MyActionName/Id1+Id2+Id3+Id4 etc.
and handle it in code in the following way:
public ActionResult MyActionName(string[] ids)
{
return View(ids);
}
+ is a reserved symbol in an url. It means white space. So to achieve what you are looking for you could write a custom model binder:
public class StringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null && !string.IsNullOrEmpty(value.AttemptedValue))
{
return value.AttemptedValue.Split(' ');
}
return base.BindModel(controllerContext, bindingContext);
}
}
and then either register it globally for the string[] type or use the ModelBinder attribute:
public ActionResult MyActionName(
[ModelBinder(typeof(StringModelBinder))] string[] ids
)
{
return View(ids);
}
Obviously if you want to use an url of the form /MyControllerName/MyActionName/Id1+Id2+Id3+Id4 that will bind the last part as an action parameter called ids you will have to modify the default route definition which uses {id}.
After all chose the following solution:
public ActionResult Action(string id = "")
{
var ids = ParseIds(id);
return View(ids);
}
private static int[] ParseIds(string idsString)
{
idsString = idsString ?? string.Empty;
var idsStrings = idsString.Split(new[] { ' ', '+' });
var ids = new List<int>();
foreach (var idString in idsStrings)
{
int id;
if (!int.TryParse(idString, out id))
continue;
if (!ids.Contains(id))
ids.Add(id);
}
return ids.ToArray();
}

How to get result from ControllerContext

My controller action returns a custom ActionResult that executes either a success or failure result depending on some validation logic. This happens within ExecuteResult.
My question is, how can I check the result?
Here's the test so far:
[TestFixture]
public class FormActionResultTests
{
TestController controller;
[SetUp]
public void SetUp()
{
ObjectFactory.Initialize(cfg =>
{
cfg.For<IFormHandler<TestModel>>().Use<TestModelHandler>();
});
controller = new TestControllerBuilder().CreateController<TestController>();
}
[Test]
public void Valid_input_returns_success_result()
{
var result = controller.Test(new TestModel { IsValid = true, IsValid2 = true })
.AssertResultIs<FormActionResult<TestModel>>();
var context = controller.ControllerContext;
result.ExecuteResult(context);
// how to verify result?
}
}
public class TestController : Controller
{
public ActionResult Test(TestModel model) {
return new FormActionResult<TestModel>(model, this.Content("Success"), View(model));
}
}
public class TestModel {
public bool IsValid { get; set; }
public bool IsValid2 { get; set; }
}
public class TestModelHandler : IFormHandler<TestModel>
{
public void Handle(TestModel form, IValidationDictionary validationDictionary)
{
}
}
Update
Here's what worked for me in the end (using NSubstitute):
[Test]
public void Valid_input_returns_success_result()
{
var result = new FormActionResult<TestModel>(new TestModel { IsValid = true, IsValid2 = true },
new ContentResult { Content = "Success" }, new ContentResult { Content = "Failed" });
var sb = new StringBuilder();
var response = Substitute.For<HttpResponseBase>();
response.When(x => x.Write(Arg.Any<string>())).Do(ctx => sb.Append(ctx.Arg<string>()));
var httpContext = Substitute.For<HttpContextBase>();
httpContext.Response.Returns(response);
var controllerContext = new ControllerContext(httpContext, new RouteData(), new TestController());
result.ExecuteResult(controllerContext);
sb.ToString().ShouldEqual("Success");
}
Controller should be tested that they return correct ActionResult in your case, and the Success or Failure of ActionResult should be tested by ActionResultTest and it has nothing to do with controller. Unit test means single unit test - but you test both controller and ActionResult in the same test, that is incorrect. To test ActionResult, first imagine that generally all that ActionResult does, is writing result to HttpResponse. Let's rewrite your code to use Moq to supply StringWriter for ControllerContext.HttpContext.HttpResponse.Output
[Test]
public void Valid_input_returns_success_result()
{
var result = controller.Test(new TestModel { IsValid = true, IsValid2 = true })
.AssertResultIs<FormActionResult<TestModel>>();
var context = controller.ControllerContext;
Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
StringWriter actionResultOutput = new StringWriter();
mockHttpContext.Setup(x => x.Response.Output).Returns(actionResultOutput);
context.HttpContext = mockHttpContext.Object;
result.ExecuteResult(context);
// how to verify result? Examine actionResultOutput
}
All is left to examine actionResultOutput. For example, if your action result is designed to return string "Success" when validation is ok and "Error" when validation failed, compare these strings to actionResultOutput.ToString(). If your result view's generated html is more complex, you can use HtmlAgilityPack to examine output more deeply
You should write a simple unit test of the Test-action, asserting on the returned action result. You shouldn't depend on the MVC framework in your test. Simply create a new instance of TestController and call the Test method.

How to unit test an ActionResult that returns a ContentResult?

I want to unit test the following ASP.NET MVC controller Index action. What do I replace the actual parameter in the assert below (stubbed with ?).
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class StatusController : Controller
{
public ActionResult Index()
{
return Content("Hello World!");
}
}
}
[TestMethod]
public void TestMethod1()
{
// Arrange
var controller = CreateStatusController();
// Act
var result = controller.Index();
// Assert
Assert.AreEqual( "Hello World!.", ? );
}
use the "as" operator to make a nullable cast. Then simply check for a null result
[TestMethod]
public void TestMethod1()
{
// Arrange
var controller = CreateStatusController();
// Act
var result = controller.Index() as ContentResult;
// Assert
Assert.NotNull(result);
Assert.AreEqual( "Hello World!.", result.Content);
}
I like creating assertion helpers for this sort of thing. For instance, you might do something like:
public static class AssertActionResult {
public static void IsContentResult(ActionResult result, string contentToMatch) {
var contentResult = result as ContentResult;
Assert.NotNull(contentResult);
Assert.AreEqual(contentToMatch, contentResult.Content);
}
}
You'd then call this like:
[TestMethod]
public void TestMethod1()
{
var controller = CreateStatusController();
var result = controller.Index();
AssertActionResult.IsContentResult(result, "Hello World!");
}
I think this makes the tests so much easier to read and write.
You cant test that the result is not null, that you receive a ContentResult and compare the values:
[TestMethod]
public void TestMethod1()
{
// Arrange
var controller = CreateStatusController();
// Act
var result = controller.Index();
// Assert
Assert.NotNull(result);
Assert.IsAssignableFrom(typeof(ContentResult), result);
Assert.AreEqual( "Hello World!.", result.Content);
}
I apoligize if the Nunit asserts aren't welformed, but look at it as pseudo-code :)

Resources