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.