Development Blog

 Sunday, February 25, 2007
« Tests + AutoMocking + IoC Container = ? | Main | Loosely-coupled Mocks »

Roy Osherove brings up several good points with regards to the preached design methodology of old and the new testable design some of us have come to know and love lately.

I think that TDD really helps to eliminate a large number of these issues. If you test everything yourself, then inherently your API is testable. Unfortunately, as Jonathan Cogley points out, TDD is not yet mainstream.

A nice side effect of having a testable API or application is that often times your code is also extensible. The thing that blocks it from being fully testable and extensible are those darn sealed and internal keywords, and of course, not making things virtual that really should be. Even open source projects are guilty of not having testable or extensible enough APIs. Yes, Castle MonoRail provides AbstractMRTestCase, but that actually uses ASP.NET and is way more heavy than I like.

Here is our base class for Controller Tests. This allows you to unit test nearly every aspect of your controller, without ever hitting ASP.NET. Notice the lovely and necessary use of reflection? In order to mock the context, you'll want to call InitializeController and pass it the controller you're testing, but be sure to do it in your _mocks.Unordered block (we almost ALWAYS use mocks.Unordered... it makes tests more readable and ensures that you're tests aren't too tightly coupled with your implementation, but that's the subject for another post.)

  public class EleutianControllerTests
  {
    #region Constants
    private const string ApplicationPhysicalPath = "Q:\\PhysicalPath";
    #endregion

    #region Member Data
    protected string _virtualDirectory = String.Empty;
    protected MockRepository _mocks;
    protected IControllerServices _controllerServices;
    protected IRailsEngineContext _context;
    protected IRequest _request;
    protected IResponse _response;
    protected IServerUtility _serverUtility;
    protected IDictionary _session;
    protected Flash _flash;
    protected NameValueCollection _parameters;
    #endregion

    #region Test Setup and Teardown Methods
    [SetUp]
    public virtual void Setup()
    {
      _mocks = new MockRepository();
      _context = _mocks.CreateMock<IRailsEngineContext>();
      _request = _mocks.DynamicMock<IRequest>();
      _response = _mocks.DynamicMock<IResponse>();
      _serverUtility = _mocks.DynamicMock<IServerUtility>();
      _session = _mocks.DynamicMock<IDictionary>();
      _flash = new Flash();
      _parameters = new NameValueCollection();
    }

    protected void InitializeController(Controller controller, string areaName, string controllerName, string actionName)
    {
      BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic |
                                  BindingFlags.InvokeMethod;

      MethodInfo method = controller.GetType().GetMethod("InitializeControllerState", bindingFlags);
      method.Invoke(controller, new object[] { areaName, controllerName, actionName });

      FieldInfo field = controller.GetType().GetField("context", bindingFlags);
      field.SetValue(controller, _context);

      field = controller.GetType().GetField("serviceProvider", bindingFlags);
      field.SetValue(controller, _context);

      InitializeRailsContext(areaName, controllerName, actionName);
    }

    protected void InitializeRailsContext(string areaName, string controllerName, string actionName)
    {
      Expect.Call(_context.UrlInfo).Return(
        new UrlInfo("eleutian.com", "www", _virtualDirectory, "http", 80,
                    Path.Combine(Path.Combine(areaName, controllerName), actionName), areaName, controllerName,
                    actionName, "rails")).Repeat.Any();
      Expect.Call(_context.GetService(typeof(IUrlBuilder))).Return(new DefaultUrlBuilder()).Repeat.Any();
      Expect.Call(_context.Server).Return(_serverUtility).Repeat.Any();
      Expect.Call(_context.Flash).Return(_flash).Repeat.Any();
      Expect.Call(_context.ApplicationPath).Return("/").Repeat.Any();
      Expect.Call(_context.Request).Return(_request).Repeat.Any();
      Expect.Call(_context.Response).Return(_response).Repeat.Any();
      Expect.Call(_context.ApplicationPhysicalPath).Return(ApplicationPhysicalPath).Repeat.Any();
      Expect.Call(_request.Params).Return(_parameters).Repeat.Any();
      Expect.Call(_context.Session).Return(_session).Repeat.Any();
    }
    #endregion
  }

Oh, and back to the original point of the post. Please, please don't mark things internal or sealed unless you have a very good reason to do it. And no, I don't think that "We can't afford to support it" is a good enough reason. Also, "We're afraid the user might break something" is DEFINITELY not a good enough reason. We know what we're getting into when we extend APIs... we're all developers here.