To get my WatiN tests working in predictable and repeatable
manner I ended up integrating Spring.NET into my application so that I could
swap out the data service implementation through configuration. When running under WatiN the stubbed data
service is used instead of the production web service. Having a predictable set of test data is critically
important, especially since some of my test assertions are looking for specific
data values in the rendered HTML, which would be impossible if I was hitting
the actual web service for data.
I integrated Spring.NET into the ASP.NET 1.1* application
I'm working on and so far I'm very happy with it. What I like about it:
- XML
configuration almost exactly the same as Spring in Java (which I've been
using pretty heavily for the last 3 months).
- Tight
ASP.NET integration, so I have no code dependencies on any of the Spring
namespaces in my code. None. Zero.
- There
are data validation libraries and other features which may be useful in
the future.
- It
just works.
So check it, here's my code behind for the Search.ascx, notice how the controller instance is set via the Controller property by Spring. Then the view instance (the Search user control instance itself) is set into the controller in the Initialize method call on page load. The one other thing that might seem strange is that I'm creating the SearchCriteria instance directly from the user control's Request collection rather than using declared ASP.Net controls; sometimes straight HTML controls are just what we need.
public class Search : ControlBase, ISearchView
{
protected System.Web.UI.WebControls.LinkButton m_btnSearch;
protected System.Web.UI.WebControls.DataGrid m_resultsGrid;
private SearchController m_controller;
private void Page_Load(object sender, System.EventArgs e)
{
Controller.Initialize(this, !Page.IsPostBack);
}
public void SortSearchResults(object sender, DataGridSortCommandEventArgs e)
{
Controller.Sort(e.SortExpression);
}
public void SearchClick(object sender, EventArgs e)
{
Controller.Search(Criteria);
}
public SearchCriteria Criteria
{
get
{
SearchCriteria criteria = new SearchCriteria();
criteria.BorrowerId = Request.Form["txtBorrowerId"];
criteria.BorrowerName = Request.Form["txtBorrowerName"];
criteria.BusinessLine = Request.Form["txtBusinessLine"];
criteria.LoanNumber = Request.Form["txtLoanNumber"];
criteria.UnderwriterUid = Request.Form["txtUnderwriterUid"];
return criteria;
}
}
/// <summary>
/// Injected by Spring.NET.
/// </summary>
public SearchController Controller
{
get { return m_controller; }
set { m_controller = value; }
}
#region ISearchView Members
public void setResults(IList results)
{
m_resultsGrid.DataSource = results;
m_resultsGrid.DataBind();
}
#endregion
}
And here's the Spring.NET specific configuration for the Search page, this does all the wiring via configuration of the different objects.
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">
<object type="Search.ascx">
<property name="Controller" ref="SearchController" />
</object>
<object id="SearchController" type="CRR.Web.Controllers.SearchController, CRR.Web">
<constructor-arg name="dataService" ref="DataService" />
<constructor-arg name="stateProvider" ref="SessionStateProviderAdapter" />
</object>
<object id="SessionStateProviderAdapter"
type="CRR.Web.Controllers.SessionStateProviderAdapter, CRR.Web"/>
<!-- Production data service implmentation -->
<!-- <object id="DataService" type="CRR.Domain.DataService, CRR.Domain"/> -->
<!-- Stubbed data service implmentation for testing -->
<object id="DataService" type="CRR.Domain.DataServiceStub, CRR.Domain"/>
</objects>
There is
also a Spring provided HttpHandler and an HttpModule for Spring in the web.config. This is the important part. By registering Spring web as the http handler for aspx pages, I'm now telling Spring to construct my aspx (and thus ascx) page instances rather than ASP.NET. With Spring now constructing my pages, its now possible to inject dependencies directly into my pages upon construction.
<httpModules>
<add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/>
</httpModules>
<httpHandlers>
<add verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web"/>
<add verb="*" path="*.asmx" type="Spring.Web.Services.WebServiceHandlerFactory, Spring.Web"/>
<add verb="*" path="ContextMonitor.ashx" type="Spring.Web.Support.ContextMonitor, Spring.Web"/>
</httpHandlers>
The search controller uses a state service, specifically ASP.NET session state via a custom session state adapter,
and a data service which are injected into the controller by Spring - the view
(code behind page) has NO knowledge of the services being injected into the
controller, as the controller instance itself is injected into the view.
Here's the relevant parts of the controller used for wiring, the constructor and Initialize(). Note that the constructor is called by Spring and that the Initialize method is called by the search view.
/// <summary>
/// Controller logic for search view.
/// </summary>
public class SearchController
{
public static readonly string ResultsKey = "SearchController.ResultsKey";
private ISearchView m_view;
private IDataService m_dataService;
private IStateProvider m_stateProvider;
public SearchController(IDataService dataService, IStateProvider stateProvider)
{
m_dataService = dataService;
m_stateProvider = stateProvider;
}
public SearchController Initialize(ISearchView view, bool firstTime)
{
m_view = view;
if (firstTime)
DefaultSearchForCurrentUser();
return this;
}
// snip //
}
The plan is to have two different Spring
configurations. One for production and
integration testing and the other one used for WatiN tests. That way when WatiN drives Internet Explorer
to verify the UI is working, it does so without ever touching the data service,
only testing specific stubs are being used for the data service. This is partly for speed but also because our
data service implementation is not yet complete.
*I know… very sad. My
last ASP.NET project was using .NET 2.0, and that was a year and a half ago
when that was started.