I have received few comments on the first post, one of them being from Rocky Lhotka the creator of the Csla.Net framework. He basically pointed to his advanced data sample (DeepData.sln available for download at www.lhotka.net).
The idea is that if we encapsulate all of the ADO.NET constructs required to fetch/update a single table into a “Data object” and move it from the Fetch() method of Csla object (some might find this similar to Table Data Gateway pattern), then the only thing we have to mock is that Data object.
In addition setting expectations would be lot simpler, since everything is encapsulated. So, instead of me talking about it lets look at how that changes the Fetch method of the ProjectList class defined in PTracker sample:
private void Fetch(string nameFilter) {
RaiseListChangedEvents = false;
DataFactory df = new DataFactory();
using(ProjectListData data = df.GetProjectListDataObject()) {
SafeDataReader dr = data.GetProjectList();
IsReadOnly = false;
while (dr.Read()) {
ProjectInfo info = new ProjectInfo(
dr.GetGuid(0),
dr.GetString(1));
// apply filter if necessary
if ((nameFilter.Length == 0) || (info.Name.IndexOf(nameFilter) == 0))
Add(info);
}
IsReadOnly = true;
}
RaiseListChangedEvents = true;
}
Code above is simpler than the original or the refactored code I had (only a single using block, and no ADO.NET dependencies) . So lets take a look at what happened. We stopped using the Database class (that functionality will move into our Data object – ProjectListData). We can see 2 new objects constructed in this code: 1. DataFactory – Factory in charge of instantiating all of the Data objects for our project 2. ProjectListData – Data object, whose purpose is to encapsulate the ADO.NET constructs, and return a SafeDataReader back to the Ftech() method.
It is important to note that ProjectListData implements IDisposable interface. That way as we dispose of it, it will dispose corresponding DataReader, DbCommand and a Connection. Hence only one using block needed here. Test are then as simple as:
[Test]
public void LoadsOne() {
Mock mockProjectListData = MockManager.Mock(typeof (ProjectListData));
mockProjectListData.ExpectAndReturn("GetProjectList",
new ProjectListFetchOneDRStub().GetDataReaderStub());
mockProjectListData.ExpectCall("Dispose");
ProjectList item = ProjectList.GetProjectList();
Assert.AreEqual(1,item.Count);
}
[Test(Description = "DataReader returns 3 items but only one should be inserted, based on filter")]
public void LoadsThreeFiltersTwo() {
Mock mockProjectListData = MockManager.Mock(typeof(ProjectListData));
mockProjectListData.ExpectAndReturn("GetProjectList",
new ProjectListFetchThreeDRStub().GetDataReaderStub());
mockProjectListData.ExpectCall("Dispose");
ProjectList item = ProjectList.GetProjectList("test");
Assert.AreEqual(1, item.Count);
}
As you can see mocking of the fetch process and replacing of the SafeDataReader is only couple of lines of code. First line defines that we intend to mock ProjectList object. Second one sets expectation that the method called GetProjectList() will be called. Result of that call is supposed to be replaced by result of the call to ProjectListFetchOneDRStub().GetDataReaderStub() in the first test, or the ProjectListFetchThreeDRStub().GetDataReaderStub() in the second test.
These two methods return our stub DataReaders that contain the test data (source code for these is available in my previous post). And that is it. Rather simple!
At the end let me show you the code of our ProjectListData object:
public class ProjectListData : IDisposable {
private SqlConnection _cn;
private SqlCommand _cm;
private SafeDataReader _data;
private bool disposed;
internal ProjectListData()
{
_cn = new SqlConnection(Database.PTrackerConnection);
_cm = _cn.CreateCommand();
_cm.CommandType = CommandType.StoredProcedure;
_cm.CommandText = "getProjects";
_data = new SafeDataReader(_cm.ExecuteReader());
}
public SafeDataReader GetProjectList()
{
return _data;
}
#region IDisposable Members
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed) {
if (disposing) {
// Dispose managed resources.
_data.Dispose();
_cm.Dispose();
_cn.Dispose();
}
// Dispose unmanaged resources
}
disposed = true;
}
#endregion
}