NHibernate Bootstrapper: Unit Tests and Project References
This post is the second one about the NHibernate Bootstrapper. The first is located here. The first post set up the project structure, introduced the generic DAO, and demonstrated the SessionPerRequest implementation in an IHttpModule. This post will factor the reference to NHibernate out of the web application project and cover some unit testing techniques. Programmers that are not familiar with SOLID should review the Wikipedia page and the references there. The first post noted that the version of NHibernate Bootstrapper presented there was not suitable for use in anything other than a demonstration program. The version of the solution discussed in this post is suitable for use in a small-scale system where there no more than 15 classes involved. The version following this post should be suitable for even the largest deployments, though there will be at least one additional post that refines the capabilities of an enterprise ready solution. The project sources are in a zip file located here and are updated to use the NHibernate 3.0.0 GA release.
PresentationManager
The solution in the first post used NHibernate references in the web application project. In this version of the solution those references have been moved to the presenter project. Now the solution is taken on the characteristics of the Model-View-Presenter (MVP) , discussed by Martin Fowler here, and later refined into a Supervising Presenter and Passive View. The solution employed here follows the Passive View pattern, where the view has no direct interaction with the model. The solution builds on 2 Code Project articles, originally released in Jul 2006. The first reference used is Model View Presenter with ASP.Net by Bill McCafferty and the second is Advancing the Model-View-Presenter Pattern – Fixing the Common Problems by Acoustic. There are a number of reasons for using the MVP pattern, but the most important of them is the enabling of testing. The second half of this post will show how it becomes possible to test the code behind of an aspx page.
In the first post the BusinessServices project, that would hold the presenters, was empty. Now there is a Presentation Manager and a Presenter classes. The PresentationManager is able to register the view of the ASP.Net page and associate it with the correct presenter. It is also remarkable in that it automatically instantiates the correct presenter for use by the ASP.Net page. This is done in the LoadPresenter method. The auto-instantiation is how ASP.Net pages are able to function with only a reference to the PresenterTypeAttribute in the web application project.
PresentationManager.cs
using System;
using Infrastructure;
namespace BusinessServices
{
publicstaticclassPresentationManager
{
publicstatic T RegisterView<T>(Type presenterType, IView myView) where T : Presenter
The PresenterTypeAttribute is what each page uses to drive SelfRegister. This is the mechanism that ties the individual web pages to the appropriate presenter in an automated fashion. This is one aspect of a poor man’s Inversion of Control without requiring a separate container to hold the various dependencies.
The code behind for the web page has been revised to work with a presenter. You will note that a large amount of code that was in the original code behind file has now been commented out, as it has been revised slightly and moved to the the PersonPresenter class. The code behind file is now left with just event declarations, a number of properties and the occasional method for working with gridview or dropdown controls and the Page_Load event. All that remains in the code behind are methods and properties that are referencing System.Web, while the various presenter classes have no reference to System.Web. It is important to note that the removal of the reference to System.Web in the presenter classes is what enables a high degree of code coverage in the Unit Tests.
// PersonDAOImpl dao = new PersonDAOImpl(m_session);
// IList<Person> people = dao.GetByCriteria(crit);
// retVal = (from person in people
// select new PersonDto
// {
// PersonID = person.Id,
// FirstName = person.FirstName,
// LastName = person.LastName,
// Email = person.Email,
// UserID = person.UserID
// }).ToList<PersonDto>();
// crit = null;
// dao = null;
// people = null;
// return retVal;
//}
Default.aspx.cs (Part 5)
publicstring txtPersonIdValue
{
get { return txtPersonID.Text; }
set { txtPersonID.Text = value; }
}
publicstring txtFirstNameValue
{
get { return txtFirstName.Text; }
set { txtFirstName.Text = value; }
}
publicstring txtLastNameValue
{
get { return txtLastName.Text; }
set { txtLastName.Text = value; }
}
publicstring txtEmailValue
{
get { return txtEmail.Text; }
set { txtEmail.Text = value; }
}
publicstring txtUserIdValue
{
get { return txtUserID.Text; }
set { txtUserID.Text = value; }
}
}
}
BasePage
The web project has had a BasePage class added to reuse element and methods common to more than just a single web page. This includes the SelfRegister method and various properties for RequestString, RequestUrl and IsPostBack. These are sample functions which have common utility throughout all the web pages. It is here that additional utility methods and functions with similar commonality would be added.
The PersonPresenter class now inherits the code that was commented out in the code behind file. It must also setup event listeners for events that will be raised from the web page. It is these event listeners that improve the testability of the solution, as now this functionality can be unit tested separate from any System.Web dependency. At this point the various presenters have a reference to NHibernate and work directly with the data access layer. The next iteration of the bootstrapper will refactor the presenter and place a data services layer between the presenter and the data access layer. The presenter will then no longer reference NHibernate.
PersonPresenter.cs (Part 1)
using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernateDAO;
using NHibernateDAO.DAOImplementations;
using DataServices.Person;
using DomainModel.Person;
using Infrastructure;
using BusinessServices.Interfaces;
namespace BusinessServices.Presenters
{
publicclassPersonPresenter : Presenter
{
privateISession m_session = null;
public PersonPresenter(IPersonView view)
: this(view, null)
{ }
public PersonPresenter(IPersonView view, IHttpSessionProvider httpSession)
The interface for the PersonView web page has also had to be revised. Here the methods that are commented out have had the implementations moved from the web page to the presenter. One new event has been added for the presenter.
IPersonView.cs
using System.Collections.Generic;
using DataServices.Person;
namespace BusinessServices.Interfaces
{
publicinterfaceIPersonView : IView
{
eventGridViewBtnEvent OnEditCommand;
eventGridViewBtnEvent OnDeleteCommand;
eventEmptyBtnEvent OnRefreshPersonGrid;
eventEmptyBtnEvent OnSaveEditPerson;
eventEmptyBtnEvent OnClearEditPerson;
//the event below had to be added for the presenter
eventEmptyEvent OnPageLoadNoPostback;
//void _view_OnEditCommand(Guid id);
//void _view_OnDeleteCommand(Guid id);
//void _view_OnRefreshPersonGrid();
//void _view_OnSaveEditPerson();
//void _view_OnClearEditPerson();
string txtPersonIdValue { get; set; }
string txtFirstNameValue { get; set; }
string txtLastNameValue { get; set; }
string txtEmailValue { get; set; }
string txtUserIdValue { get; set; }
void Fill_gvPerson(IList<PersonDto> data);
//IList<PersonDto> Get_PersonData();
}
}
Data Access Objects Improvements
The data access objects have been expanded to include support for NHibernate LINQ, which is now part of the core. Also support for selection of an unique object has been included, rather than always returning an IList. This means that there has been an update to the IRead.cs file as shown below.
IRead.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DomainModel;
using NHibernate;
namespace NHibernateDAO
{
publicinterfaceIRead<TEntity> where TEntity : Entity
public TEntity GetUniqByQueryable(IQueryable<TEntity> queryable)
{
if (!m_Session.Transaction.IsActive)
{
TEntity retval;
using (var tx = m_Session.BeginTransaction())
{
retval = queryable.Single<TEntity>();
tx.Commit();
return retval;
}
}
else
{
return queryable.Single<TEntity>();
}
}
GenericDAOImpl.cs (Pt 4)
public TEntity Save(TEntity entity)
{
if (!m_Session.Transaction.IsActive)
{
using (var tx = m_Session.BeginTransaction())
{
m_Session.SaveOrUpdate(entity);
tx.Commit();
}
}
else
{
m_Session.SaveOrUpdate(entity);
}
return entity;
}
}
}
Unit Test
The unit testing capabilities of the solution have been expanded. It starts with a fake PersonView class in the unit test project that includes functionality similar to that in the web page, but implemented without any reference to System.Web
PersonView.cs (Part 1)
using System;
using System.Collections.Generic;
using BusinessServices;
using BusinessServices.Interfaces;
using BusinessServices.Presenters;
using DataServices.Person;
namespace BootstrapperUnitTests.Views
{
[PresenterType(typeof(PersonPresenter))]
publicclassPersonView : BaseView, IPersonView
{
privatebool blnRegistered = false;
privatestring _txtPersonID;
privatestring _txtFirstName;
privatestring _txtLastName;
privatestring _txtEmail;
privatestring _txtUserID;
privatestaticbool postBack;
privateIList<PersonDto> _gvPerson;
publiceventGridViewBtnEvent OnEditCommand;
publiceventGridViewBtnEvent OnDeleteCommand;
publiceventEmptyBtnEvent OnRefreshPersonGrid;
publiceventEmptyBtnEvent OnSaveEditPerson;
publiceventEmptyBtnEvent OnClearEditPerson;
publiceventEmptyEvent OnPageLoadNoPostback;
public PersonView()
{
postBack = false;
base.SelfRegister(this);
blnRegistered = true;
}
///<summary>
/// This is used by the test subsystem to avoid the self-registry action and allow normal
/// object creation.
///</summary>
///<param name="noRegister"></param>
public PersonView(bool noRegister)
{
blnRegistered = false;
postBack = false;
}
PersonView.cs (Part 2)
publicvoid Fill_gvPerson(IList<PersonDto> data)
{
_gvPerson = data;
}
protectedinternalIList<PersonDto> GvPerson
{
get { return _gvPerson; }
}
publicvoid FireEvent_OnEditCommand(Guid id)
{
OnEditCommand(id);
}
publicvoid FireEvent_OnDeleteCommand(Guid id)
{
OnDeleteCommand(id);
}
publicvoid FireEvent_OnRefreshPersonGrid()
{
OnRefreshPersonGrid();
}
publicvoid FireEvent_OnSaveEditPerson()
{
if (_txtUserID != null)
{
OnSaveEditPerson();
}
}
publicvoid FireEvent_OnClearEditPerson()
{
OnClearEditPerson();
}
publicvoid FireEvent_OnPageLoadNoPostback()
{
OnPageLoadNoPostback();
}
PersonView.cs (Part 3)
publicstring txtPersonIdValue
{
get { return _txtPersonID; }
set { _txtPersonID = value; }
}
publicstring txtFirstNameValue
{
get { return _txtFirstName; }
set { _txtFirstName = value; }
}
publicstring txtLastNameValue
{
get { return _txtLastName; }
set { _txtLastName = value; }
}
publicstring txtEmailValue
{
get { return _txtEmail; }
set { _txtEmail = value; }
}
publicstring txtUserIdValue
{
get { return _txtUserID; }
set { _txtUserID = value; }
}
boolIView.IsPostback
{
get
{
if (!postBack)
{
postBack = true;
returnfalse;
}
else
returntrue;
}
}
}
}
BasePage.cs must also be copied to the unit test project, and it also no longer references System.Web. It is renamed to BaseView.cs in the actual solution. You will note that the property IsPostBack throws a NotImplementedException, but it could be set to a read/write property and provide values through an external property setter in the tests.
The tests themselves have been expanded and now show a considerably increased code coverage. In doing so the tests are now a combination of unit tests and integration tests, but I would argue that this is desirable as it increases the overall reliability of the solution, which is the purpose of automated tests.
PersonPresenterTests.cs (Pt 1)
using System;
using System.Collections.Generic;
using NUnit.Framework;
using DomainModel.Person;
using NHibernate;
using NHibernate.Context;
using NHibernateDAO;
using NHibernateDAO.DAOImplementations;
using BootstrapperUnitTests.Views;
using BootstrapperUnitTests.TestData;
using BusinessServices.Interfaces;
using BusinessServices.Presenters;
using DataServices.Person;
namespace BootstrapperUnitTests.PresenterTests
{
[TestFixture]
publicclassPersonPresenterTests
{
privateISession m_session;
[TestFixtureSetUp]
publicvoid TestFixtureSetup()
{
var session = SessionManager.SessionFactory.OpenSession();
CallSessionContext.Bind(session);
}
[TestFixtureTearDown]
publicvoid TestFixtureTeardown()
{
PersonTestData ptd = newPersonTestData();
ptd.RemoveAllPersonEntries();
var session = CallSessionContext.Unbind(SessionManager.SessionFactory);
if (session != null)
{
if (session.Transaction != null && session.Transaction.IsActive)
IList<Person> people = daoPerson.GetByCriteria(crit);
Assert.Greater(people.Count, 0);
Int32 pers = people.Count;
Console.WriteLine("Count After Add: {0}", pers);
people = null;
daoPerson.Delete(newPerson);
newPerson = null;
people = daoPerson.GetByCriteria(crit);
Console.WriteLine("Count after Delete: {0}", people.Count);
Assert.IsTrue(pers.Equals(people.Count + 1));
people = null;
_person = null;
daoPerson = null;
m_session = null;
}
}
}
Summary
This solution retains all the functionality as was included in the original solution, but it has now been placed in an architecture that improves testability and simplifies the creation of functionality as each element now has a clear project where it belongs within the solution. The next evolution of the bootstrapper will investigate data service usage and more complex mappings. However this version of the bootstrapper is now appropriate to use for simple projects with NHibernate where the domain model is relatively simple.
If you remember back to the code behind file discussion there were still methods that remained for working with the gridview and drop down controls on the web page. These could be moved up to the presenter if these controls were redesigned as composite controls in a separate project in the solution so that they were no longer inherited through System.Web.
Posted Sun, 05 December 2010 05:59:00 AM
by jwdavidson