DEV Community

Cover image for SparkyTestHelpers: AspNetMvc
Brian Schroer
Brian Schroer

Posted on • Edited on

SparkyTestHelpers: AspNetMvc

.NET Framework version:
NuGet package | Source code | API documentation

.NET Core version:
NuGet package | Source code | API documentation

These NuGet packages (one for the “classic” ASP.NET MVC Framework and one for ASP.NET Core MVC) contain helpers for testing that MVC controller actions return the expected results:


Controller Testers

ControllerTester<T> tests action methods of controllers that inherit from System.Web.MVC.Controller / Microsoft.AspNetCore.Mvc.ControllerBase.

ApiControllerTester<T> tests action methods of controllers that inherit from the .NET Framework System.Web.Http.ApiController class.

The general syntax is:

tester
    .Action(action selection expression)
    .When(optional code to "arrange" test conditions)
    .Expecting...(optional code to set up assert expectations)
    .Test...()

Examples

//ControllerTester:

    moviesControllerTester
        .Action(x => x.Index)
        .TestView();

    moviesControllerTester
        .Action(x => () => x.Details(3))
        .ExpectingViewName("Details")
        .ExpectingModel<Movle>(movie => Assert.AreEqual("Office Space", movie.Title))
        .TestView();

    moviesControllerTester
        .Action(x => x.Edit(testInvalidModel))
        .WhenModelStateIsValidEquals(false)
        .TestRedirectToAction("Errors");

    moviesControllerTester
        .Action(x => () => x.Edit(testValidModel))
        .WhenModelStateIsValidEquals(true)
        .ExpectingViewName("UpdateSuccessful")
        .TestRedirectToRoute("Home/UpdateSuccessful");

//ApiControllerTester:

   moviesApiControllerTester 
        .Action<IEnumerable<Movie>>(x => x.GetAllMovies)
        .Test(movies => Assert.AreEqual(100, movies.Count());

   moviesApiControllerTester 
        .Action(x => () => x.Get)
        .WhenRequestHasQueryStringParameters(new QueryStringParameter("id", 3))
        .TestOkNegotiatedContentResult<Movie>(movie => Assert.AreEqual("Office Space", movie.Title));

    moviesApiControllerTester
        .Action(x => () => x.Update(updateModel))
        .WhenModelStateIsValidEquals(false)
        .TestBadRequestResult();

    moviesApiControllerTester
        .Action(x => () => x.Update(updateModel))
        .WhenModelStateIsValidEquals(true)
        .TestOkResult();
Enter fullscreen mode Exit fullscreen mode

Instantiation

ControllerTester<T> and ApiControllerTester<T> can be created either via their constructors:

var homeControllerTester = new ControllerTester<HomeController>(testHomeController);
var moviesApiControllerTester = new ApiControllerTester<MoviesApiController>(testMoviesController);
Enter fullscreen mode Exit fullscreen mode

...or via CreateTester extension methods:

var homeControllerTester = testHomeController.CreateTester();
var moviesApiControllerTester = testMoviesController.CreateTester();
Enter fullscreen mode Exit fullscreen mode

"Action" Methods

tester.Action methods use lambda expressions to specify the controller action to be tested. The expressions enable Intellisense completion when you "dot on" to the controller argument.

The syntax for parameterless actions is ".Action(controller => controller.actionName)", e.g. .Action(x => x.Index).

For actions with parameters, the syntax is ".Action(controller => () => controller.actionName(arguments))", e.g. .Action(x => () => x.Get(3)).

"When" methods

  • WhenRequestHasQueryStringParameters(string siteUrlPrefix, NameValueCollection queryStringParameters)
  • WhenRequestHasQueryStringParameters(NameValueCollection queryStringParameters)
  • WhenRequestHasQueryStringParameters(string siteUrlPrefix, params QueryStringParameter[] queryStringParameters)
  • WhenRequestHasQueryStringParameters(params QueryStringParameter[] queryStringParameters)

The WhenRequestHasQueryStringParameters methods (ApiControllerTester only) set up the controller's Request property with a RequestUri containing the specified parameters. If the siteUrlPrefix isn't specified (it usually won't matter in unit tests), the value "http://localhost" is used.

  • WhenModelStateIsValidEquals(bool isValid)

This method sets up the controller's ModelState for testing.

  • When(Action action)

This method can be used to "arrange" any conditions necessary for the test, e.g. setting up a mocked interface method.

"Expecting" methods (ControllerTester only):

  • ExpectingViewName(string expectedViewName)

ExpectingViewName sets up automatic validation when followed by TestView or TestPartialView

  • ExpectingModel<TModel>(Action<*TModel> validate*)

ExpectingModel sets up automatic model type and possibly content (the validate callback action is optional) validation when followed by TestView or TestJson.

"Test" methods - ControllerTester:

ControllerTester<TController> has several .Test... methods used to assert that the controller action returns the expected ActionResult implementation (The method name suffixes correspond to ...Result types (e.g. TestView tests ViewResult). There are methods for all of the standard result types, plus the generic TestResult<TActionResultType> method.

The validate "callback" actions, which can be used to validate the result contents, are optional:

  • TestContent(Action<ContentResult> validate)
  • TestEmpty(Action<EmptyResult> validate)
  • TestFile(Action<FileResult> validate)
  • TestJson(Action<JsonResult> validate)
  • TestPartialView(Action<PartialViewResult> validate)
  • TestRedirect(string expectedUrl, Action<RedirectResult> validate)
  • TestRedirectToAction(string expectedActionName, Action<RedirectToRouteResult> validate)
  • TestRedirectToRoute(string expectedRoute, Action<RedirectToRouteResult> validate)
  • TestView(Action<ViewResult> validate)
  • TestResult<TActionResultType>(Action<TActionResultType> validate)

"Test" methods - ApiControllerTester - for actions that return an IHttpActionResult:

There several .Test... methods used to assert that API controller actions return the expected IHttpActionResult implementation. There are methods for all of the standard result types, plus the generic TestResult<THttpActionResultType> method:

The validate "callback" actions, which can be used to validate the result contents, are optional:

  • TestBadRequestErrorMessageResult(Action<BadRequestErrorMessageResult> validate)
  • TestBadRequestResult(Action<BadRequestResult> validate)
  • TestConflictResult(Action<ConflictResult> validate)
  • TestCreatedAtRouteNegotiatedContentResult<T>(Action<CreatedAtRouteNegotiatedContentResult<T>> validate)
  • TestCreatedNegotiatedContentResult<T>(Action<CreatedNegotiatedContentResult<T>> validate)
  • TestExceptionResult(Action<ExceptionResult> validate)
  • TestFormattedContentResult<T>(Action<FormattedContentResult<T>> validate)
  • TestInternalServerErrorResult(Action<InternalServerErrorResult> validate)
  • TestInvalidModelStateResult(Action<InvalidModelStateResult> validate)
  • TestJsonResult<T>(Action<JsonResult<T>> validate)
  • TestNegotiatedContentResult<T>(Action<NegotiatedContentResult<T>> validate)
  • TestNotFoundResult(Action<NotFoundResult> validate)
  • TestOkNegotiatedContentResult<T>(Action<OkNegotiatedContentResult<T>> validate)
  • TestOkResult(Action<OkResult> validate)
  • TestRedirectResult(Action<RedirectResult> validate)
  • TestRedirectToRouteResult(Action<RedirectToRouteResult> validate)
  • TestResponseMessageResult(Action<ResponseMessageResult> validate)
  • TestStatusCodeResult(Action<StatusCodeResult> validate)
  • TestUnauthorizedResult(Action<UnauthorizedResult> validate)
  • TestResult<THttpActionResultType>(Action<THttpActionResultType> validate)

"Test" methods - ApiControllerTester - for actions that return an HttpResponseMessage:

The validate "callback" actions, which can be used to validate the result contents, are optional:

  • Test(Action<HttpResponseMessage> validate)

...calls controller action, validates HttpResponseMessage.StatusCode (if ExpectingHttpStatusCode(HttpStatusCode) has been called) and returns the HttpResponseMessage returned from the action.

  • TestContentString(Action<string> validate)

...calls controller action, validates HttpResponseMessage.StatusCode (if ExpectingHttpStatusCode(HttpStatusCode) has been called) and returns the HttpResponseMessage.Content string.

  • TestContentJsonDeserialization<TContent>(Action<TContent> validate)

...calls controller action, validates HttpResponseMessage.StatusCode (if ExpectingHttpStatusCode(HttpStatusCode) has been called) and returns the HttpResponseMessage.Content's JSON string deserialized to a TContent instance.


Testing ASP.NET MVC Core RazorPages

PageModelTester<TPageModel>

ASP.NET MVC Razor Page PageModels have a lot in common with Controllers (they kind of combine the “C” and “M” of MVC), so the Razor PageModelTester has similar Action, When..., ExpectingModel and *Test... methods:

Test… methods assert that the PageModel action returns the expected IActionResult implementation. There are methods for many standard result types, plus the generic TestResult<TActionResultType> method:

  • TestContent((optional) Action<ContentResult> validate)
  • TestFile((optional) Action<FileResult> validate)
  • TestJsonResult((optional) Action<JsonResult> validate)
  • TestPage((optional) Action<PageResult> validate)
  • TestRedirectToAction(string expecteActionName, string expectedControllerName, object expectedRouteValues, (optional) Action validate)
  • TestRedirectToPage(string expectedPageName, (optional) Action<RedirectToPageResult> validate)
  • TestRedirectToRoute(string expectedRoute, (optional) Action<RedirectToRouteResult> validate)
  • TestResult<TActionResultType>((optional) Action<TActionResultType> validate)

The validate “callback” methods may be used for additional data assertions, beyond testing proper return types.

Examples:

var homeModel = new HomeModel(/* with test dependencies */);

var pageTester = new PageModelTester<HomeModel>(homeModel);

pageTester
    .Action(x => x.OnGet)
    .ExpectingModel(model => Assert.IsTrue(model.Foo))
    .TestPage();

pageTester
    .Action(x => x.OnPost)
    .WhenModelStateIsValidEquals(false)
    .ExpectingModel(model => 
        Assert.AreEqual(expectedErrorMessage, model.ErrorMessage))
    .TestPage();

pageTester
    .Action(x => x.Post)
    .WhenModelStateIsValidEquals(true)
    .TestRedirectToPage("UpdateSuccessful");
Enter fullscreen mode Exit fullscreen mode

Testing ASP.NET MVC Core ViewComponents

The .NET Core framework for ASP.NET MVC has another nice new tool that "framework" MVC doesn't: ViewComponents, a powerful alternative to partial views.

The ViewComponent test helper classes have a lot of similarities with their Controller and Razor PageModel counterparts:

ViewComponentTester methods

Invocation methods

Similar to the ControllerTester and PageModelTester "Action" methods:

  • Invocation(synchronous Invoke method expression)
  • Invocation(async InvokeAsync method expression)

"When" methods

  • WhenModelStateIsValidEquals(bool isValid)

This method sets up the ViewComponent's ModelState for testing.

  • When(Action action)

This method can be used to "arrange" any conditions necessary for the test, e.g. setting up a mocked interface method.

Expecting methods

  • .ExpectingViewName(string expectedViewName) — used with .TestView
  • ExpectingModel<TModelType>(Action<TModelType> validate) — used with .TestView

Test methods

  • TestContent(Action<ContentViewComponentResult> validate)
  • TestHtmlContent(Action<HtmlContentViewComponentResult> validate)
  • TestView(Action<ViewViewComponentResult> validate)
  • TestResult<TViewComponentResultType>(Action<TViewComponentResultType> validate)
  • WhenModelStateIsValidEquals(bool isValid) — used to test conditional logic based on ModelState.IsValid

All validate “callback” actions shown above are optional.

Example code

using SparkyTestHelpers.AspNetMvc
. . .

new ViewComponentTester<FooViewComponent>()
    .Invocation(x => x.Invoke)
    .ExpectingViewName("Default")
    .ExpectingModel(model => Assert.IsTrue(model.Baz))
    .TestView();

new ViewComponentTester<FooViewComponent>()
    .Invocation(x => x.Invoke)
    .WhenModelStateIsValidEquals(false)
    .ExpectingViewName("Errors")
    .TestView();

new ViewComponentTester<BarViewComponent>()
    .Invocation(x => x.InvokeAsnyc)
    .TestView();
Enter fullscreen mode Exit fullscreen mode

Testing Routing

These test helpers are only in the .NET Framework NuGet package. Routing has been rewritten for .NET Core, and I’ll have to figure out how to mock a bunch of different dependencies before I can add routing test helpers for .NET Core. It looks like Ivaylo Kenov has figured it out, so if you want to unit test ASP.NET Core routing now, I suggest you look at his GitHub repo.

RouteTester

RouteTester and RoutingAsserter provide methods to assert that a given relative URL maps to the expected RouteData.Values. The RoutingAsserter.AssertMapTo overloads provide multiple ways to specify the expected values…

RouteTester Constructors

  • public RouteTester((Action<RouteCollection> routeRegistrationMethod*)
  • public RouteTester(AreaRegistration areaRegistration)
using SparkyTestHelpers.AspNetMvc.Routing;
. . .
  var routeTester = new RouteTester(RouteConfig.RegisterRoutes);
  var areaRouteTester = new RouteTester(new FooAreaRegistration());
Enter fullscreen mode Exit fullscreen mode

RouteTester Methods

  • .ForUrl(string relativeUrl) — creates a new ##RoutingAsserter## instance.

RoutingAsserter

Nethods

  • AssertMapTo(IDictionary<string, object> expectedValues)
  • AssertMapTo(object routeValues)
  • AssertMapTo(string controller, string action, (object id)) — id defaults to null
  • AssertMapTo<TController>(Expression<Func<TController, Func<ActionResult>>> actionExpression)
  • AssertRedirectTo(string expectedUrl, (HttpStatusCode expectedStatusCode)) — expectedStatusCode defaults to HttpStatusCode.Redirect (302)

Examples

routeTester.ForUrl("Default.aspx")
  .AssertRedirectTo("Home/LegacyRedirect");

// alternate syntaxes for asserting Home/Index routing:

routeTester.ForUrl("Home/Index")
  .AssertMapTo(new Dictionary<string, object> 
  {{ "controller", "Home" }, { "action", "Index" }, { "id", null });
routeTester.ForUrl("Home/Index")
  .AssertMapTo(new {controller = "Home", action = "Index"});
routeTester.ForUrl("Home/Index")
  .AssertMapTo("Home", "Index");
routeTester.ForUrl("Home/Index")
  .AssertMapTo<HomeController>(x => x.Index);

// alternate syntaxes for asserting Order/Details/3 routing:

routeTester.ForUrl("Order/Details/3")
  .AssertMapTo(new Dictionary<string, object> 
    {{"controller", "Order"}, {"action", "Details"}, {"id", 3});
routeTester.ForUrl("Order/Details/3")
  .AssertMapTo(
    new {controller = "Order", action = "Details", id = 3 });
routeTester.ForUrl("Order/Details/3")
  .AssertMapTo("Order", "Details", 3);
routeTester.ForUrl("Order/Details/3")
  .AssertMapTo<OrderController>(x => () => x.Details(3));
Enter fullscreen mode Exit fullscreen mode

Happy Testing!

Top comments (0)