A pattern for unit testable Asp.net pages: Part 4
Okay, now for unit tests of the code from part 3. The goal is to completely cover RenamePageModel. First we need mock implementations of IPageContext and IDataAccess. For IPageContext, we are going to use special exception types for page not found and server errors. And, we'll add an accessor to track redirects. It's all pretty straightforward.
public class MockPageContext : IPageContext
{
public bool IsSecureConnection
{
get { return _isSecureConnection; }
set { _isSecureConnection = value; }
}
public bool IsPost
{
get { return _isPost; }
set { _isPost = value; }
}
public void ThrowPageNotFound()
{
throw new MockPageNotFoundException();
}
public void ThrowServerError()
{
throw new MockServerErrorException();
}
public void Redirect(string destination)
{
_redirectString = destination;
}
public bool HasRedirected
{
get { return _redirectString != null; }
}
public string RedirectUrl
{
get { return _redirectString; }
}
private bool _isSecureConnection;
private bool _isPost;
private string _redirectString;
}
public class MockPageNotFoundException : Exception
{
}
public class MockServerErrorException : Exception
{
}
The IDataAccess mock is also pretty straightforward. It's simply got properties that can be used to set the return value for the function, or to throw exceptions if desired.
class MockDataAccess : IDataAccess
{
public MockDataAccess(string getItemNameResult, Exception getItemNameException, string setItemNameResult, Exception setItemNameException)
{
_getItemNameResult = getItemNameResult;
_getItemNameException = getItemNameException;
_setItemNameResult = setItemNameResult;
_setItemNameException = setItemNameException;
}
public string GetItemName(string path)
{
if (_getItemNameException != null)
{
throw _getItemNameException;
}
return _getItemNameResult;
}
public string SetItemName(string path, string newName)
{
if (_setItemNameException != null)
{
throw _setItemNameException;
}
return _setItemNameResult;
}
private string _getItemNameResult;
private Exception _getItemNameException;
private string _setItemNameResult;
private Exception _setItemNameException;
}
And, now for the actual unit tests. Basically there's a test for each code path on load or post.
[TestClass]
public class RenamePageModelTests
{
[TestMethod]
public void TestLoadSuccess()
{
MockDataAccess mockDataAccess = new MockDataAccess("name", null, null, null);
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(new MockPageContext());
Assert.AreEqual("name", model.Name, "Name not set from item name");
}
[TestMethod]
[ExpectedException(typeof(MockPageNotFoundException))]
public void TestLoadFileNotFoundException()
{
MockDataAccess mockDataAccess = new MockDataAccess("name", new FileNotFoundException(), null, null);
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(new MockPageContext());
}
[TestMethod]
[ExpectedException(typeof(MockPageNotFoundException))]
public void TestLoadUnauthorizedAccessException()
{
MockDataAccess mockDataAccess = new MockDataAccess("name", new UnauthorizedAccessException(), null, null);
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(new MockPageContext());
}
[TestMethod]
[ExpectedException(typeof(MockServerErrorException))]
public void TestLoadIOException()
{
MockDataAccess mockDataAccess = new MockDataAccess("name", new IOException(), null, null);
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(new MockPageContext());
}
[TestMethod]
public void TestPostSuccess()
{
MockPageContext pageContext = new MockPageContext();
pageContext.IsPost = true;
MockDataAccess mockDataAccess = new MockDataAccess("name", null, "newpath", null);
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(pageContext);
Assert.AreEqual("rename.aspx?path=newpath", pageContext.RedirectUrl, "Did not redirect to correct url");
}
[TestMethod]
public void TestPostArgumentException()
{
MockPageContext pageContext = new MockPageContext();
pageContext.IsPost = true;
MockDataAccess mockDataAccess = new MockDataAccess("name", null, null, new ArgumentException());
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Name = "errorName";
model.Load(pageContext);
Assert.AreEqual("errorName", model.Name, "New name not returned in model.Name");
Assert.IsNotNull(model.ErrorString, "Error string not set");
}
[TestMethod]
[ExpectedException(typeof(MockPageNotFoundException))]
public void TestPostFileNotFoundException()
{
MockPageContext pageContext = new MockPageContext();
pageContext.IsPost = true;
MockDataAccess mockDataAccess = new MockDataAccess("name", null, null, new FileNotFoundException());
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(pageContext);
}
[TestMethod]
[ExpectedException(typeof(MockPageNotFoundException))]
public void TestPostUnauthorizedAccessException()
{
MockPageContext pageContext = new MockPageContext();
pageContext.IsPost = true;
MockDataAccess mockDataAccess = new MockDataAccess("name", null, null, new UnauthorizedAccessException());
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(pageContext);
}
[TestMethod]
[ExpectedException(typeof(MockServerErrorException))]
public void TestPostIOException()
{
MockPageContext pageContext = new MockPageContext();
pageContext.IsPost = true;
MockDataAccess mockDataAccess = new MockDataAccess("name", null, null, new IOException());
RenamePageModel model = new RenamePageModel(mockDataAccess, "foo");
model.Load(pageContext);
}
If you use Visual Studio .Net's handy code coverage coloring, you'll see that we are covering all the code in the model, except for some closing tags in exceptions that are never hit because of the IPageContext functions that throw exceptions.
Well, that's it! I've put the entire project on Windows Live SkyDrive:
Let me know if you have any questions on it. And, let me know if there are other areas you'd like me to write about, such as using the asynchronous model. I'm interested to hear feedback on how this model can be adapted for other types of web apps.
Comments
Anonymous
July 31, 2007
I previously blogged about the pattern we used in our WPF application to separate business logic fromAnonymous
August 20, 2007
I followed with great interest the series of articles “DataModel-View-ViewModel pattern”? This one uses is focused on framework WPF. Does this new series of articles focused on ASP.NET use same DataModel-View-ViewModel pattern? Or is this another pattern style MVC? Isn't DataModel-View-ViewModel pattern usable also with ASP.NET? In advance thank you. JMAnonymous
August 21, 2007
This is a good question. When I started doing Asp.net work, I naturally tried to apply the same model. However, Asp.net is a very different model from WPF, which is natural given the nature of web pages. In terms of mapping this pattern to the DataModel-View-ViewModel, the PageModel is basically the same as the ViewModel. It's the unit testable model behind a view (or page) that exposes properties for the view to bind to. With Asp.net, the .aspx is basically the same as the View in my previous pattern. The mechanisms for bindings and dealing with input are somewhat different, but they are conceptually the same. I really miss the concept of content controls and item templates when working with asp.net. One of the major problems in WPF was doing expensive network operations on a background thread to not block the UI. This issue does not really exist with a web page. A web page (at least a non-AJAX web page) gets a request, does any expensive operations, and then renders some HTML back. So, the DataModel portion doesn't really exist, or is at least very simplified. I hope this helps!