Unit Testing in the Navigation for ASP.NET Web Forms Framework


Graham Mendick

Download the Code Sample

Download the Library

The Navigation for ASP.NET Web Forms framework, an open source project hosted at navigation.codeplex.com, opens up new possibilities for writing Web Forms applications by taking a new approach to navigation and data passing. In traditional Web Forms code, the way data is passed is dependent on the navigation performed. For example, it might be held in the query string or route data during a redirection, but in control values or view state during a post back. However, in the Navigation for ASP.NET Web Forms framework (“Navigation framework” hereafter for brevity) a single data source is used in all scenarios.

In my first article (msdn.microsoft.com/magazine/hh975349), I introduced the Navigation framework and built a sample online survey application to demonstrate some of its key concepts and the advantages it provides. In particular, I showed how it enabled the generation of a set of dynamic, context-sensitive breadcrumb hyperlinks allowing a user to return to previously visited questions with the user’s answers restored, overcoming the limitations of the static ASP.NET site map functionality.

Also in this first article, I claimed the Navigation framework lets you write Web Forms code that would make an ASP.NET MVC application green with envy. However, the sample survey application didn’t bear this claim out because the code was crammed into the codebehinds and impenetrable to unit testing.

I’ll set matters straight in this second article, editing the survey application so that it’s just as well-structured as a typical ASP.NET MVC application and with a higher level of unit testability. I’ll use standard ASP.NET data binding together with the Navigation framework to clear out the codebehinds and extract the business logic into a separate class, which I’ll then unit test. This testing won’t require any mocking and the code coverage will include the navigation logic—features rarely provided by other ASP.NET unit-testing approaches.

Data Binding

The graveyard of Web Forms code is littered with the bloated corpses of codebehind files, but it doesn’t have to be this way. Although Web Forms has featured data binding since its inauguration, it was in 2005 that Visual Studio introduced data source controls and the Bind syntax for two-way updatable binding, allowing the development of Web Forms applications more akin in structure to a typical MVC application. The beneficial effects of such code, particularly with regard to unit testing, are widely recognized, reflected in the fact that the majority of the Web Forms development effort for the next version of Visual Studio has been spent in this area.

To demonstrate, I’ll take the survey application developed in the first article and convert it to an MVC-like architecture. A controller class will hold the business logic and ViewModel classes will hold the data for communication between the controller and the views. It will require very little development effort because the code currently in the codebehinds can be cut and pasted almost verbatim into the controller.

Starting with Question1.aspx, the first step is to create a Question ViewModel class containing a string property so the selected answer can be passed to and from the controller:

  1. public class Question
  2. {
  3.   public string Answer
  4.   {
  5.     get;
  6.     set;
  7.   }
  8. }

Next comes the controller class, which I’ll call SurveyController; a Plain Old CLR Object, unlike an MVC controller. Question1.aspx needs two methods, one for the data retrieval that returns the Question ViewModel class and one for the data update that accepts the Question ViewModel class:

  1. public class SurveyController
  2. {
  3.   public Question GetQuestion1()
  4.   {
  5.     return null;
  6.   }
  7.   public void UpdateQuestion1(Question question)
  8.   {
  9.   }
  10. }

To flesh out these methods I’ll use the code in the codebehind of Question1.aspx, moving the page load logic into GetQuestion1 and the button click handler logic into UpdateQuestion1. Because the controller doesn’t have access to the controls on the page, the Question ViewModel class is used to get and set the answer rather than the radio button list. The GetQuestion1 method requires a further tweak to ensure the default answer returned is “Web Forms”:

  1. public Question GetQuestion1()
  2. {
  3.   string answer = “Web Forms”;
  4.   if (StateContext.Data[“answer”] != null)
  5.   {
  6.     answer = (string)StateContext.Data[“answer”];
  7.   }
  8.   return new Question() { Answer = answer };
  9. }

In MVC, data binding is at the request level with the request mapped to a controller method via route registration, but in Web Forms, data binding is at the control level with the mapping done using an ObjectDataSource. So to hook Question1.aspx up to the SurveyController methods, I’ll add a FormView connected to an appropriately configured data source:

  1. <asp:FormView ID=”Question” runat=”server”
  2.   DataSourceID=”QuestionDataSource” DefaultMode=”Edit”>
  3.   <EditItemTemplate>
  4.   </EditItemTemplate>
  5. </asp:FormView>
  6. <asp:ObjectDataSource ID=”QuestionDataSource”
  7.   runat=”server” SelectMethod=”GetQuestion1″
  8.   UpdateMethod=”UpdateQuestion1″ TypeName=”Survey.SurveyController”
  9.   DataObjectTypeName=”Survey.Question” />

The final step is to move the question, consisting of the radio button list and button, inside the EditItemTemplate of the FormView. At the same time, two changes must be made in order for the data-binding mechanism to work. The first is to use the Bind syntax so the answer returned from GetQuestion1 is displayed and the newly selected answer is passed back to UpdateQuestion1. The second is to set the CommandName of the button to Update so UpdateQuestion1 will be automatically called when it’s pressed (you’ll notice the Selected attribute of the first list item has been removed because setting the default answer to “Web Forms” is now managed in GetQuestion1):

  1. <asp:RadioButtonList ID=”Answer” runat=”server”
  2.   SelectedValue='<%# Bind(“Answer”) %>’>
  3.   <asp:ListItem Text=”Web Forms” />
  4.   <asp:ListItem Text=”MVC” />
  5. </asp:RadioButtonList>
  6. <asp:Button ID=”Next” runat=”server”
  7.   Text=”Next” CommandName=”Update” />

The process is complete for Question1.aspx, and its codebehind is gratifyingly empty. The same steps can be followed to add data binding to Question2.aspx, but its codebehind can’t be completely cleared because the page load code related to the back navigation hyperlink must remain there for the time being. In the next section, where the integration of the Navigation framework with data binding is discussed, it will be moved into the markup and the codebehind vacated.

Adding data binding to Thanks.aspx is similar, but rather than reuse the inappropriately named Question ViewModel class, I’ll create a new one called Summary with a string property to hold the answers selected:

  1. public class Summary
  2. {
  3.   public string Text
  4.   {
  5.     get;
  6.     set;
  7.   }
  8. }

Because Thanks.aspx is a read-only screen, only a data retrieval method is required on the controller and, as with Question2.aspx, all the page load code aside from the back navigation logic can be moved into this method:

  1. public Summary GetSummary()
  2. {
  3.   Summary summary = new Summary();
  4.   summary.Text = (string)StateContext.Data[“technology”];
  5.   if (StateContext.Data[“navigation”] != null)
  6.   {
  7.     summary.Text += “, ” + (bool)StateContext.Data[“navigation”];
  8.   }
  9.   return summary;
  10. }

Because no update functionality is required, the FormView ItemTemplate is used instead of EditItemTemplate, and the syntax for one-way binding, Eval, is used in place of Bind:

  1. <asp:FormView ID=”Summary” runat=”server”
  2.   DataSourceID=”SummaryDataSource”>
  3.   <ItemTemplate>
  4.     <asp:Label ID=”Details” runat=”server”
  5.       Text='<%# Eval(“Text”) %>’ />
  6.   </ItemTemplate>
  7. </asp:FormView>
  8. <asp:ObjectDataSource ID=”SummaryDataSource”
  9.   runat=”server”
  10.   SelectMethod=”GetSummary”
  11.   TypeName=”Survey.SurveyController” />

Half the unit testing battle is won because the survey application business logic has been extracted into a separate class. However, because the code has been pasted into the controller virtually unchanged from the codebehind, the power of data binding isn’t yet fully utilized.

Navigation Data Binding

The survey application code still has a couple problems: Only the update methods in the SurveyController should contain navigational logic, and the codebehinds aren’t empty. Unit testing shouldn’t begin until these issues are resolved, as the former would result in unnecessarily complex unit tests for the get methods and the latter would prevent 100 percent unit test coverage.

The select parameters of data source controls make accessing the HttpRequest object in data-bound methods redundant. For example, the QueryStringParameter class allows query string data to be passed as parameters to data-bound methods. The Navigation framework has a NavigationDataParameter class that performs the equivalent job for the state data on the StateContext object.

Equipped with this NavigationDataParameter, I can revisit GetQuestion1, removing all the code that accesses the state data by making the answer a method parameter instead. This significantly simplifies the code:

  1. public Question GetQuestion1(string answer)
  2. {
  3.   return new Question() { Answer = answer ?? “Web Forms” };
  4. }

The accompanying change to Question1.aspx is to add the NavigationDataParameter to its data source. This involves first registering the Navigation namespace at the top of the page:

  1. <%@ Register assembly=”Navigation”
  2.                        namespace=”Navigation”
  3.                         tagprefix=”nav” %>

Then the NavigationDataParameter can be added to the data source’s select parameters:

  1. <asp:ObjectDataSource ID=”QuestionDataSource” runat=”server”
  2.   SelectMethod=”GetQuestion1″ UpdateMethod=”UpdateQuestion1″
  3.   TypeName=”Survey.SurveyController”
  4.   DataObjectTypeName=”Survey.Question” >
  5.   <SelectParameters>
  6.     <nav:NavigationDataParameter Name=”answer” />
  7.   </SelectParameters>
  8. </asp:ObjectDataSource>

The GetQuestion1 method, having been stripped of all Web-specific code, is now easily unit tested. The same can be done for GetQuestion2.

For the GetSummary method, two parameters are needed, one for each answer. The second parameter is a bool to match how the data is passed by UpdateQuestion2, and it must be nullable because the second question isn’t always asked:

  1. public Summary GetSummary(string technology, bool? navigation)
  2. {
  3.   Summary summary = new Summary();
  4.   summary.Text = technology;
  5.   if (navigation.HasValue)
  6.   {
  7.     summary.Text += “, ” + navigation.Value;
  8.   }
  9.   return summary;
  10. }

And the corresponding change to the data source on Thanks.aspx is the addition of the two NavigationDataParameters:

  1. <asp:ObjectDataSource ID=”SummaryDataSource” runat=”server”
  2.   SelectMethod=”GetSummary” TypeName=”Survey.SurveyController” >
  3.   <SelectParameters>
  4.     <nav:NavigationDataParameter Name=”technology” />
  5.     <nav:NavigationDataParameter Name=”navigation” />
  6.   </SelectParameters>
  7. </asp:ObjectDataSource>

The first problem with the survey application code has been addressed because now only the update methods in the controller contain navigational logic.

You’ll recall that the Navigation framework improves on the static breadcrumb navigation functionality provided by the Web Forms site map, keeping track of the states visited together with their state data and building up a context-sensitive breadcrumb trail of the actual route taken by the user. To construct back navigation hyperlinks in markup—without needing codebehind—the Navigation framework provides a CrumbTrailDataSource analogous to the SiteMapPath control. When used as the backing data source for a ListView, the CrumbTrailDataSource returns a list of items, one per previously visited state, with each containing a NavigationLink URL that allows context-sensitive navigation back to that state.

I’ll use this new data source to move the Question2.aspx back navigation into its markup. First, I’ll add a ListView connected up to the CrumbTrailDataSource:

  1. <asp:ListView ID=”Crumbs” runat=”server”
  2.   DataSourceID=”CrumbTrailDataSource”>
  3.   <LayoutTemplate>
  4.     <asp:PlaceHolder ID=”itemPlaceholder” runat=”server” />
  5.   </LayoutTemplate>
  6.   <ItemTemplate>
  7.   </ItemTemplate>
  8. </asp:ListView>
  9. <nav:CrumbTrailDataSource ID=”CrumbTrailDataSource” runat=”server” />

Next, I’ll delete the page load code from the Question2.aspx code­behind, move the back navigation hyperlink inside the ListView ItemTemplate and use the Eval binding to populate the NavigateUrl property:

  1. <asp:HyperLink ID=”Question1″ runat=”server”
  2.   NavigateUrl='<%# Eval(“NavigationLink”) %>’ Text=”Question 1″/>

You’ll notice that the HyperLink Text property is hardcoded to “Question 1.” This works perfectly well for Question2.aspx because the only possible back navigation is to the first question. However, the same can’t be said for Thanks.aspx because it’s possible to return to either the first or second question. Fortunately, the navigation configuration entered in the StateInfo.config file allows a title attribute to be associated to each state, such as:

  1. <state key=”Question1″ page=”~/Question1.aspx” title=”Question 1″>

And then the CrumbTrailDataSource makes this title available to data binding:

  1. <asp:HyperLink ID=”Question1″ runat=”server”
  2.   NavigateUrl='<%# Eval(“NavigationLink”) %>’
  3.   Text='<%# Eval(“Title”) %>’/>

Applying these same changes to Thanks.aspx addresses the second problem with the survey application code because all the codebehinds are now empty. However, all this effort will be wasted if the SurveyController can’t be unit tested.

Unit Testing

With the survey application now nicely structured—codebehinds are empty and all UI logic is in the page markup—it’s time to unit test the SurveyController class. The GetQuestion1, GetQuestion2 and GetSummary data-retrieval methods clearly can be unit tested, as they contain no Web-specific code. Only the UpdateQuestion1 and Update­Question2 methods present a unit testing challenge. Without the Navigation framework, these two methods would contain routing and redirection calls—the traditional way to move and pass data between ASPX pages—which both throw exceptions when used outside of a Web environment, causing unit testing to trip at the first hurdle. However, with the Navigation framework in place, these two methods can be fully unit tested without requiring any code change or mock objects.

For starters, I’ll create a Unit Test project for the survey. Right-clicking inside any method in the SurveyController class and selecting the “Create Unit Tests …” menu option will create a project with the necessary references included and a Survey­ControllerTest class.

You’ll recall that the Navigation framework requires the list of states and transitions to be configured in the StateInfo.config file. In order for the unit test project to use this same navigation configuration, the StateInfo.config file from the Web project must be deployed when the unit tests are executed. With this in mind, I’ll double-click the Local.testsettings solution item and select the “Enable deployment” checkbox under the Deployment tab. Then I’ll decorate the SurveyControllerTest class with the DeploymentItem attribute referencing this StateInfo.config file:

  1. [TestClass]
  2. [DeploymentItem(@”Survey\StateInfo.config”)]
  3. public class SurveyControllerTest
  4. {
  5. }

Next, an app.config file must be added to the test project pointing at this deployed StateInfo.config file (this configuration is also required in the Web project, but it was automatically added by the NuGet installation):

<configuration>   <configSections>     <sectionGroup name="Navigation">       <section name="StateInfo" type=         "Navigation.StateInfoSectionHandler, Navigation" />     </sectionGroup>   </configSections>   <Navigation>     <StateInfo configSource="StateInfo.config" />   </Navigation> </configuration>

With this configuration in place, unit testing can begin. I’ll follow the AAA pattern for structuring a unit test:

  1. Arrange: Set up the preconditions and test data.
  2. Act: Execute the unit under test.
  3. Assert: Verify the result.

Starting with the UpdateQuestion1 method, I’ll show what’s required at each of these three steps when it comes to testing the navigation and data passing in the Navigation framework.

The Arrange step sets up the unit test, creating the object under test and the parameters that need to be passed to the method under test. For UpdateQuestion1, this means creating a SurveyController and a Question populated with the relevant answer. However, an extra navigational setup condition is required, mirroring the navigation that occurs when the Web application is started. When the survey Web application is started, the Navigation framework intercepts the request for the startup page, Question1.aspx, and navigates to the dialog whose path attribute matches this request in the StateInfo.config file:

  1. <dialog key=”Survey” initial=”Question1″ path=”~/Question1.aspx”>

Navigating using a dialog’s key goes to the state mentioned in its initial attribute, so the Question1 state is reached. Because it isn’t possible to set a startup page in a unit test, this dialog navigation must be performed manually and is the extra condition required in the Arrange step:

  1. StateController.Navigate(“Survey”);

The Act step calls the method under test. This just involves passing the Question with its answer populated to UpdateQuestion1 and so doesn’t require any navigation-specific details.

The Assert step compares the results against expected values. Verifying the outcomes of navigating and data passing can be done using classes in the Navigation framework. You’ll recall that the StateContext provides access to state data via its Data property, which is initialized with the NavigationData passed during navigation. This can be used to verify that UpdateQuestion1 passes the answer selected to the next state. So, assuming “Web Forms” is passed into the method, the Assert becomes:

  1. Assert.AreEqual(“Web Forms”, (string) StateContext.Data[“technology”]);

The StateContext also has a State property that tracks the current state. This can be used to check if a navigation occurred as expected—for example, that passing “Web Forms” into UpdateQuestion1 should navigate to Question2:

  1. Assert.AreEqual(“Question2”, StateContext.State.Key);

While the StateContext holds details about the current state and associated data, the Crumb is the equivalent class for previously visited states and their data—so-called because each time a user navigates, a new one is added to the breadcrumb trail. This breadcrumb trail or list of crumbs is accessible via the Crumbs property of the StateController (and is the backing data of the CrumbTrailDataSource of the previous section). I need recourse to this list to check that UpdateQuestion1 stores the passed answer in its state data before navigating, because once the navigation occurs, a crumb is created holding this state data. Assuming the answer passed in is “Web Forms,” the data on the first and only crumb can be verified:

  1. Assert.AreEqual(“Web Forms”, (string) StateController.Crumbs[0].Data[“answer”]);

The AAA pattern of writing a structured unit test has been covered regarding the Navigation framework. Putting all these steps together, following is a unit test to check if the Question2 state is reached after passing an answer of “Web Forms” to UpdateQuestion1 (with a blank line inserted between the separate steps for clarity):

  1. [TestMethod]
  2. public void UpdateQuestion1NavigatesToQuestion2IfAnswerIsWebForms()
  3. {
  4.   StateController.Navigate(“Survey”);
  5.   SurveyController controller = new SurveyController();
  6.   Question question = new Question() { Answer = “Web Forms” };
  7.   controller.UpdateQuestion1(question);
  8.   Assert.AreEqual(“Question2”, StateContext.State.Key);
  9. }

Although that’s all you need to be able to unit test the different Navigation framework concepts, it’s worth continuing with UpdateQuestion2 because it has a couple of differences in its Arrange and Act steps. The navigation condition required in its Arrange step is different because, to call UpdateQuestion2, the current state should be Question2 and the current state data should contain the “Web Forms” technology answer. In the Web application this navigation and data passing is managed by the UI because the user can’t progress to the second question without answering “Web Forms” to the first question. However, in the unit test environment, this must be done manually. This involves the same dialog navigation required by UpdateQuestion1 to reach the Question1 state, followed by a navigation passing the Next transition key and “Web Forms” answer in NavigationData:

  1. StateController.Navigate(“Survey”);
  2. StateController.Navigate(
  3.   “Next”, new NavigationData() { { “technology”, “Web Forms” } });

The only difference in the Assert step for UpdateQuestion2 comes when verifying its answer is stored in state data prior to navigation. When this check was done for UpdateQuestion1, the first crumb in the list was used because only one state had been visited, namely Question1. However, for UpdateQuestion2, there will be two crumbs in the list because both Question1 and Question2 have been reached. Crumbs appear in the list in the order they’re visited, so Question2 is the second entry and the requisite check becomes:

  1. Assert.AreEqual(“Yes”, (string)StateController.Crumbs[1].Data[“answer”]);

The lofty ambition of unit-testable Web Forms code has been attained. This was done using standard data binding with assistance from the Navigation framework. It’s less prescriptive than other ASP.NET unit testing approaches because the controller hasn’t had to inherit or implement any framework class or interface, and its methods haven’t had to return any particular framework types.

Is MVC Jealous Yet?

MVC should be feeling some pangs of jealousy because the survey application is just as well-structured as a typical MVC application, but has a higher level of unit testing. The survey application’s navigational code appears inside the controller methods and is tested along with the rest of the business logic. In an MVC application, the navigational code isn’t tested because it’s contained within the return types of the controller methods, such as the RedirectResult. In my next article on the Navigation framework, I’ll compound MVC’s jealousy by building a Search Engine Optimization-friendly, single-page application with adherence to don’t repeat yourself, or DRY, principles that’s difficult to achieve in its MVC equivalent.

That said, Web Forms data binding does have problems that aren’t present in its MVC counterpart. For example, it’s difficult to use dependency injection on the controller classes, and nested types on the ViewModel classes aren’t supported. But Web Forms has learned a lot from MVC, and the next version of Visual Studio will see a vastly improved Web Forms data-binding experience.

There’s much more to the Navigation framework’s integration with data binding than is shown here. For example, there’s a data pager control that—unlike the ASP.NET DataPager—doesn’t need hooking up to a control or require a separate count method. If you’re interested in finding out more, comprehensive documentation and sample code are available at navigation.codeplex.com.

Graham Mendick is Web Forms’ biggest fan and wants to show it can be just as architecturally sound as ASP.NET MVC. He authored the Navigation for ASP.NET Web Forms framework, which he believes—when used with data binding—can breathe new life into Web Forms.

Thanks to the following technical expert for reviewing this article: Scott Hanselman

This entry was posted in Achitecture, ASP.NET, UT. Bookmark the permalink.


Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s