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 This is not ASP.NET Core MVC. ASP.NET MVC 5 is the pre-Core, 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.