In-Depth

Unit Testing and ASP.NET MVC

ASP.NET MVC is important to all Microsoft .NET Framework developers for at least two reasons.

First, it reflects the first Microsoft commitment to the Model-View-Controller (MVC) pattern for implementing applications. The MVC pattern supports loose coupling and a better separation of concerns among an application's components, so the pattern is critical to creating complex apps.

Second, ASP.NET MVC is important because of test-driven development (TDD). Maintaining quality and reliability in software products still depends, ultimately, on inspection (testing). TDD bakes the testing process by converting requirements directly into test code. With TDD, developers build to meet the demands of the tests rather than the requirements.

In addition to these general benefits, ASP.NET MVC provides specific benefits for Web developers. The primary one is a better development platform for JavaScript client-side programming than ASP.NET.

To make all of this magic happen, the MVC pattern breaks an application up into three components. In ASP.NET MVC, the Model (the middle-tier business objects) and the Controller (which controls the Views) can be tested using the Visual Studio Unit Testing Framework. Testing the View, however, is problematic.

View Issues
The View presents the data to the user. In the MVC pattern, the goal is to create Views so brain-dead simple that they require no testing. If a View builds successfully then any problems that remain are, at worst, "blunders" (such as spelling errors or data in the wrong place) rather than complicated logic errors. Errors in the View are assumed to be trivial -- easily spotted and easily fixed.


[Click on image for larger view.]
Figure 1. Unit tests that exercise the Actions in Controllers can be run individually or as a batch, with the results consolidated into a single report.

The problem in Web applications is that the HTML page displayed in the client is the View. But one of the primary benefits provided by ASP.NET MVC is that it provides a platform for client-side programming. Instead of becoming simpler, thanks to JavaScript and jQuery, the HTML View is becoming increasingly more sophisticated.

Taking a comprehensive look at unit testing in ASP.NET MVC requires looking at two currently separate issues: server-side testing (Model and Controller) and client-side testing (View).

Testing Controllers
Beause testing middle-tier business objects -- the Model -- is the same in ASP.NET MVC as in any other application, we'll ignore it for this article. While there are special objects for testing Controllers, Controllers can be tested using the standard TDD code and following the standard paradigm.

The first step in testing Controllers is to create a typesafe View: a View that's passed a custom class from the Controller and returns a customer class to the Controller. These objects -- effectively, Data Transfer Objects (DTOs) -- are property-rich classes that must be added to the site's Models folder. A typical DTO looks like this:

Namespace Models
    Public Class CustomerData
        Public Property ContactName As String
    End Class
End Namespace

Once the DTO is defined, you create the method in a Controller that will cause the View to be sent to the browser. The simplest possible controller class that uses the CustomerData DTO just defined would look like this:

Namespace Sample_MVC
  Public Class CustomerController
        Inherits System.Web.Mvc.Controller

    Public Function ManageCustomerData(
        CustIn As Models.CustomerData) As ActionResult
      Return View(Nothing)
    End Function

  End Class
End Namespace

The final step is to create the associated View by right-clicking on the Controller method and selecting Add View. In the resulting dialog, pick the DTO class to generate a View bound to the DTO. The resulting View is tied to DTO through its Page directive:

<%@ Page Language="VB" 
 Inherits="System.Web.Mvc.ViewPage(
 Of Sample_MVC.Models.CustomerData)" %>

Once the View is created you can use the ASP.NET MVC HTML Helpers to generate the tags and code that make up the View. The goal here is to create a page that passes a DTO back to the Controller method called when the page posts back. To simplify this example, this code returns the same CustomerData object passed to the View when it's selected:

<% using (Html.BeginForm()) {%>
         
<fieldset>
    <legend>Edit Customer</legend>
    <%: Html.EditorFor(model => model.CustomerData,
        new { ContactName = model.CustomerData}) %>
    <p>
        <input type="submit" value="Save" />
    </p>
</fieldset>

With that in the View, the Controller method -- now accepting a CustomerData object -- would look like this:

Public Function ManageCustomerData(
              CustIn As Models.CustomerData) As ActionResult
   Return View(Nothing)
End Function

The Controller class is, in many ways, a class like any other class. For instance, in TDD, the first test for any class is always to see if the class can be instantiated:

<TestMethod()> _
Public Sub CreateCustomerController()
Dim cdc As CustomerController =
         New CustomerController
  Assert.IsNotNull(cdc,"Unable to create controller")
End Sub

The second test in the TDD paradigm is to determine if the class' first method can be called and if it will accept parameters. No Assert is required for this test: If the test runs to completion, it's a success, and if the code blows up ... well, that's not a good sign:

<TestMethod()> _
Public Sub CallGetCustomerData()
Dim cdc As CustomerController =
         New CustomerController
Dim cd As New Models.CustomerData With 
        {.ContactName = "Peter Vogel"}
        
  cdc.GetCustomerData(cd)
End Sub

The third test is to determine if a result can be returned from a minimal implementation of the Controller method. The minimal implementation for this Controller method would be to return the CustomerData object passed to the method, as this code does:

Public Function GetCustomerData(
     ByVal CustIn As Models.CustomerData) As ActionResult

  Return View(CustIn)
End Function

A Controller method always returns a result of type ActionResult, so the third test looks like this:

<TestMethod()> _
Public Sub ReturnGetCustomerData ()
Dim cdc As CustomerController =
         New CustomerController
Dim cd As New Models.CustomerData With
        {.ContactName = "Peter Vogel"}
        
  Assert.IsNotNull(cdc,"Unable to get result ")
End Sub

The fourth test is to see if the result returned from the method contains the correct values. This is the point at which testing a Controller requires some special consideration. While the Controller method returns an ActionResult object, the test needs to access the DTO that will, eventually, be passed to the View. To access the DTO, first check to see if the return result can be converted to a ViewResult. If it can, then extract the DTO and check to see if the DTO has the expected data in its properties:

  <TestMethod()> _
  Public Sub CheckReturnGetCustomerData()
  Dim cdc As CustomerController =
           New CustomerController
  Dim cd As New Models.CustomerData With 
          {.ContactName = "Peter Vogel"}
  Dim ar As ActionResult
        
    ar = cdc.GetCustomerData(cd)

    Assert.IsInstanceOfType(
      ar, GetType(ViewResult))
    Dim vr As ViewResult = CType(
      ar, ViewResult)
  cd = CType(vr.ViewData.Model, Models.CustomerData)
  Assert.AreEqual("Peter Vogel", cd.ContactName)
End Sub

The next set of tests would support testing the functionality coded into the Controller by providing different inputs to the Controller. As this progression shows, even one of the components that's special to ASP.NET MVC -- the Controller -- is compatible with the TDD process. Even ASP.NET MVC routing can be tested using the Visual Studio Unit Testing Framework.

Unit Testing in the Client
You can do TDD with client-side code, but it's a different world from the server-side experience. To begin with, there's no testing framework baked into Visual Studio and, instead, you'll have to choose from several different competing JavaScript-based tools. The closest that any of these packages comes to being the "approved" framework is QUnit, which is used by the jQuery team.


[Click on image for larger view.]
Figure 2. A QUnit test runner page. A test runner page isn't subtle when an error is found: The failed test is displayed with its message and the test is highlighted in red.

JavaScript functions can be divided into two groups. First, there are the business/processing functions. These include AJAX calls to the server to retrieve data from the server or send data to the server for processing. Integrating AJAX calls is even easier in the upcoming ASP.NET MVC 3 (see "Looking to ASP.NET MVC 3").

Then there's the UI code that interacts with the View by retrieving data for the business functions, updating the View with retrieved data or just modifying the View based on the user's input. These changes can include modifying either the View's structure or its appearance.

It's easy to intermix this client-side code: A single function might include the code to retrieve data from the View (UI logic), send that data to a service on the client (business logic), and update the View (UI logic again) to report success or failure. Testing, therefore, not only means understanding the testing framework -- it helps structure your code to enable you to test as much of your code as possible. The first step, for instance, is to move as much of your JavaScript code as possible out of your View and into .JS files.

Setting up and Executing Client-Side Testing
To implement QUnit testing, create a Web site with subfolders for Scripts, Stylesheets and Tests. To the Scripts and Stylesheets folder add the two components that make up QUnit: qunit.js and qunit.css (you can get these files and read about QUnit at docs.jquery.com/qunit). In my Tests subfolder, I'll place the JavaScript files with the code that will check my View's JavaScript code.

To check my JavaScript code I use the QUnit test function, which accepts two parameters: A name for the test and a function that executes my tests. The test function calls my View's JavaScript functions and then uses a QUnit assertion to check the result. This example calls a getCustomerData function and then uses the QUnit equals assertion to see if the correct customer was retrieved:

test('getCustomerData',
      function () {
          getCustomerData("ALFKI");
          equals(custData.Name, "ALFKI", 
                 'Customer data retrieved');}
    )

With a test written, add an HTML test runner page to your test Web site. The test runner page needs to reference the QUnit stylesheet and include several heading/div tags that QUnit will update to display test results. It must also reference the qunit.js file, the file with the test code (Tests.js in this case) and the JavaScript file containing the View functions to test (UI.js for this example). Here's a typical test runner page:

<!DOCTYPE html>  
<html>  
 <head>  
  <title>UI Tests</title>  
    <link rel="stylesheet" 
        href="../ViewTests/StyleSheets/qunit.css" 
 	     type="text/css" media="screen"/>  
    <script type="text/javascript" 
        src="../ViewTests/Scripts/qunit.js"></script>      
    <script type="text/javascript" 
        src="../ViewTests/Tests/Tests.js"></script>  
    <script type="text/javascript" 
        src="http://MySite/Scripts/Project/UI.js"></script>         
 </head>  
 <body>  
   <h1 id="qunit-header">UI Tests</h1>  
   <h2 id="qunit-banner"></h2>  
   <div id="qunit-testrunner-toolbar"></div>  
   <h2 id="qunit-userAgent"></h2>  
   <ol id="qunit-tests"></ol>  
 </body>  
</html> 

To run the tests, just display the test runner page. QUnit takes care of executing all the tests in any script libraries and updating the test runner page with the results (see Figure 2). This test, however, won't work because the getCustomerData function calls a Web service asynchronously. A test written like this will run the assertion before any data is returned from the Web Service.

Fortunately, asynchronous testing is supported in QUnit. To test asynchronously, use the QUnit stop function to pause your test function. Then call your assertions from the QUnit setTimeout function that will restart your tests. The second parameter to the setTimeout function specifies how long you'll wait before running your assertion.

This example of an asynchronous test waits one second before checking the results of GetCustomerData:

test('getCustomerData', 
     function() {
       getCustomerData("ALFKI");
       stop();
       setTimeout(function (){
                      equals(custData.Name, "ALFKI", 
                             'Customer data retrieved');
                      start();},
                  1000)
      }
) 

Refactoring for Testing
While there's more to QUnit than I've covered here, the product's capabilities aren't the real issue: Structuring your code to support testing is. For instance, while the test proves that the function can retrieve the data for a specified customer name, it doesn't prove that the code will interact successfully with the View. These interactions would include using jQuery selectors to pull the customer name from the View and to update the View with the results retrieved from the Web service.

To support TDD, the code that uses jQuery selectors to find elements in the View must be factored into functions separate from both the business and UI logic. Those "selector functions" are called by the functions with the business and UI logic -- which can now be tested independently of the View. The jQuery selector functions may be difficult to test but will, hopefully, be so simple that testing isn't required. For instance, code that would bind a call to the getCustomer-Data function to a View element might look like this:

$("#customerList").bind("click", 
   function (e) {
     getCustomerData(
        this.value, customerRetrieved, genericFailure);
    }
  );

The customerRetrieved function called here would update the View with the data returned from the Web service. The customerRetrieved function should be structured so that it first calls a "selector function" that finds the element in the View that is to be updated and returns it to customerRetrieved. After customerRetrieved updates the retrieved element, the function would pass it to a second function that finds the appropriate part of the View and inserts the results. The customerRetrieved function would look like this:

function customerRetrieved(button) {
   var divElm = getDataTable();            
    ...update divElm...
   updateDataTable(divElm);
 }

During testing, the selector function that finds the element would be replaced with a stub that returns a dummy element, as this code does:

function getDataTabel() {
  return $("<div>");
}

In the "real" View, updateDataTable would find the appropriate element in the View, delete all of its contents and insert the version created in customerRetrieved. For testing, the updateDataTable stub that accepts the updated element would contain the assertions which check that the element was modified correctly.

To support testing, three JavaScript files are required: one to hold the View's functions, one to hold the selector functions used in the real View, and a file to hold the stub functions used in testing (this file would be part of the test Web site). The production View would use the first two files, while the test runner would use the first and third.

A New Paradigm
ASP.NET MVC makes TDD easier to do on the server. However, while client-side testing is possible, it's not as convenient or as easy. Part of the issue is built into the environment: For security reasons, to test calls to a Web service, the test code may have to be part of the production site rather than being in a separate site. While this article has only discussed loading the test runner page into a single browser, for Internet (rather than intranet) applications, the View tests would need be executed in several browsers and the results consolidated into a single report.

Frameworks do exist for this, but are not yet part of Visual Studio. The good news here is that ASP.NET MVC 3 and Razor are intended to facilitate testing outside of a Web page. Looking at the issues involved from the point of view of a server-side developer, client-side testing still remains the next frontier for the Visual Studio Unit Testing Framework.

What's needed isn't new technology so much as a reassessment that no longer thinks of "everything in the Web pages" as a View but, instead, thinks of it as a client with its own implementation of the MVC pattern.

In this paradigm, the browser has a relatively simple Model that grabs DTO when the View is selected on the server and from Web services on the client. There would also be a JavaScript Controller that would be called from the View as the user interacts with the View. The Controller would make all changes and would call simple functions to integrate those changes into the View. The refactorings required for testing purposes are the start of that paradigm.

After all, if MVC makes sense on the server, then it also makes sense to use it for the increasingly more sophisticated clients that users -- and employers -- expect.

comments powered by Disqus

Featured

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube