Testing and Diagnostics

This section documents ASP.NET MVC 5.3.x, ASP.NET Web API 2.2, ASP.NET Web Pages 3, OWIN/Katana, SignalR 2, and ASP.NET Identity 2 — all running on .NET Framework 4.8.1 — the System.Web-hosted MVC framework, its routing, Razor views, HTML helpers, model binding, filters, and the OWIN-based authentication/Identity stack — as described by the official documentation at Microsoft Learn (plus Web API, Web Pages, SignalR, and Identity), which are the reference these pages are written and verified against.

This is not ASP.NET Core MVC. ASP.NET MVC 5 is the pre-Core, System.Web-hosted MVC framework; it is functionally frozen and receives only security fixes. For the current, cross-platform MVC framework see MVC Controllers and Views under ASP.NET Core (Blazor).

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page documents testing ASP.NET MVC 5.3.x applications on .NET Framework 4.8.1 — not ASP.NET Core’s WebApplicationFactory-based integration testing (see Testing under ASP.NET Core).

Unit-testing controllers

Because Controller.Execute returns an ActionResult object rather than writing straight to a response stream, actions are testable by simply calling the method and asserting on the result’s type and data — `ControllerBase’s design specifically exists to make this possible without a running web server:

[TestMethod]
public void Details_UnknownId_ReturnsHttpNotFound()
{
    var controller = new ProductsController(new FakeProductRepository());
    var result = controller.Details(999);
    Assert.IsInstanceOfType(result, typeof(HttpNotFoundResult));
}

[TestMethod]
public void Index_ReturnsViewWithAllProducts()
{
    var controller = new ProductsController(new FakeProductRepository());
    var result = controller.Index() as ViewResult;
    Assert.IsNotNull(result);
    var model = result.ViewData.Model as IEnumerable<Product>;
    Assert.AreEqual(3, model.Count());
}

See Controllers and Actions for the full ActionResult family being asserted on here.

Mocking HttpContextBase/ControllerContext/HttpRequestBase

An action that reads Request, Response, Server, or User needs a fake HttpContextBase — the abstraction MVC introduced over System.Web.HttpContext precisely so tests never need a real IIS/System.Web runtime:

var request = new Mock<HttpRequestBase>();
request.Setup(r => r.HttpMethod).Returns("GET");
request.Setup(r => r.UserHostAddress).Returns("127.0.0.1");

var context = new Mock<HttpContextBase>();
context.Setup(c => c.Request).Returns(request.Object);

var controller = new ProductsController(new FakeProductRepository())
{
    ControllerContext = new ControllerContext(context.Object, new RouteData(), new Mock<ControllerBase>().Object)
};

Session, Response.Cookies, and User are mocked the same way when an action touches them.

Testing routes

[TestMethod]
public void Route_ProductDetails_MapsToProductsController()
{
    RouteCollection routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);

    var httpContext = new Mock<HttpContextBase>();
    httpContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns("~/products/42");
    httpContext.Setup(c => c.Request.PathInfo).Returns(string.Empty);

    RouteData routeData = routes.GetRouteData(httpContext.Object);

    Assert.AreEqual("Products", routeData.Values["controller"]);
    Assert.AreEqual("Details", routeData.Values["action"]);
    Assert.AreEqual("42", routeData.Values["id"]);
}

Building a fresh RouteCollection and calling the real RegisterRoutes (rather than asserting against RouteTable.Routes directly) keeps the test independent of application startup order — see Routing and Areas.

Testing filters and helpers

A filter’s On*Executing/On*Executed methods take a context object and can be exercised directly:

[TestMethod]
public void RequireHttps_HttpRequest_SetsRedirectResult()
{
    var filter = new RequireHttpsAttribute();
    var context = BuildAuthorizationContext(isSecure: false);   // test helper building an AuthorizationContext
    filter.OnAuthorization(context);
    Assert.IsInstanceOfType(context.Result, typeof(RedirectResult));
}

Custom HtmlHelper extension methods (see HTML Helpers and Forms) are tested by constructing an HtmlHelper over a mocked ViewContext/ViewDataContainer and asserting on the returned `MvcHtmlString’s text.

Testing Web API controllers

ApiController actions are tested the same way as MVC actions, asserting on IHttpActionResult:

[TestMethod]
public void Post_ValidProduct_ReturnsCreatedAtRoute()
{
    var controller = new ProductsController(new FakeProductRepository())
    {
        Request = new HttpRequestMessage(),
        Configuration = new HttpConfiguration()
    };
    var result = controller.Post(new Product { Name = "Widget" }) as CreatedAtRouteNegotiatedContentResult<Product>;
    Assert.IsNotNull(result);
    Assert.AreEqual("Widget", result.Content.Name);
}

See ASP.NET Web API 2 for IHttpActionResult and its factory helpers.

Integration tests with OWIN TestServer

Microsoft.Owin.Testing.TestServer spins up an in-memory OWIN pipeline (see Authentication, Identity, and OWIN) for true end-to-end HTTP tests against Web API/OWIN-hosted middleware without a real IIS process:

[TestMethod]
public async Task Api_Products_ReturnsOk()
{
    using (var server = TestServer.Create<Startup>())
    {
        var response = await server.HttpClient.GetAsync("/api/products");
        response.EnsureSuccessStatusCode();
    }
}

This covers Web API and OWIN middleware but not System.Web-hosted MVC controllers directly, since MVC’s own pipeline is not OWIN-hosted in MVC 5 (see The MVC Pattern and Request Life Cycle) — an MVC UI is instead typically covered by the Selenium tests below.

UI tests with Selenium

[TestMethod]
public void Login_ValidCredentials_RedirectsToDashboard()
{
    using (var driver = new ChromeDriver())
    {
        driver.Navigate().GoToUrl("https://localhost:44300/Account/Login");
        driver.FindElement(By.Id("Email")).SendKeys("user@example.com");
        driver.FindElement(By.Id("Password")).SendKeys("Password1!");
        driver.FindElement(By.CssSelector("button[type=submit]")).Click();
        Assert.IsTrue(driver.Url.Contains("/Dashboard"));
    }
}

Selenium WebDriver drives a real browser against a running (typically IIS Express or a deployed test environment) instance — the only reliable way to exercise client-side validation (see Model Binding and Validation) and JavaScript behavior end to end.

ELMAH and Application_Error

ELMAH (Error Logging Modules and Handlers) is an HttpModule that logs every unhandled exception and exposes a browsable /elmah.axd error log, with almost no application code required:

<!-- web.config -->
<httpModules>
  <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
</httpModules>
<elmah>
  <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="DefaultConnection" />
</elmah>

Global.asax’s `Application_Error remains the place to add custom handling on top of ELMAH’s automatic logging (e.g. routing certain exception types to a custom error page):

protected void Application_Error()
{
    var exception = Server.GetLastError();
    if (exception is HttpException httpEx && httpEx.GetHttpCode() == 404)
        Response.Redirect("~/Error/NotFound");
}

[HandleError] (see Filters) catches exceptions within the MVC pipeline and shows a friendly view; Application_Error catches everything else, including exceptions MVC’s own filter pipeline never sees.

Tracing and <system.diagnostics>

<system.diagnostics>
  <trace autoflush="true" indentsize="2">
    <listeners>
      <add name="textListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="trace.log" />
    </listeners>
  </trace>
</system.diagnostics>
Trace.TraceInformation($"Processing order {order.Id}");

System.Diagnostics.Trace/TraceSource predate structured logging frameworks and are still commonly present in MVC 5 codebases, sometimes alongside a dedicated library (log4net, NLog) added independently. See Testing ASP.NET MVC applications and the ELMAH project.

Next: Migrating to ASP.NET Core is the last page in this section, covering the path off MVC 5 entirely.