Ayende @ Rahien

Unnatural acts on source code

Advance Mocking - Partial mocks and Do()

Kyle has just posted about the MvcContrib.TestHelper project:

The CreateController method on my TestControllerBuilder uses Castle's DynamicProxy to create the controller. It also creates a ControllerContext using dynamic mocks from Rhino Mocks similar to the way Haackselman have already showed you and wires it up to the controller.

An interceptor is also attached to the controller to intercept calls to RenderView and RedirectToAction. (Actually, it intercepts all calls but only these two have special handling.) Calls to either method will populate an appropriate object on the builder class. In the example above, I'm using the RedirectToActionData object.

I'll leave aside the need to get to that level when you want to test brand new framework and code. I think that this is a serious flaw, but that is not the subject of this post. The use of dynamic proxy and interceptors caught my eye, because they aren't actually needed. Rhino Mocks can do it for you. Let us see how it is done right now.

public T CreateController<T>() where T : Controller
{
      ProxyGenerator generator = new ProxyGenerator(); //Castle DynamicProxy
      Controller controller = (Controller)generator.CreateClassProxy(typeof(T), new ControllerInterceptor(this));

      ControllerContext controllerContext = new ControllerContext(HttpContext, RouteData, controller);
      controller.ControllerContext = controllerContext;
     
      typeof(T).GetProperty("TempData").SetValue(controller, TempDataDictionary, null);

      return controller as T;
}

The controller interceptor can be found here. Let us take out the dynamic proxy part and use Rhino Mocks to handle it. In this case, it will look like this:

public T CreateController<T>(params object[] ctorArgs) where T : Controller
{
	Controller controller = _mocks.PartialMock<T>();
	
	var renderViewDataParams = new Type[]{typeof(string),typeof(string), typeof(object));
	typeof(Controller).GetMethod("RenderViewData", renderViewDataParams)
		.Invoke(controller, new [] { null, null, null });
	LastCall.IgnoreArguments().Repeat.Any()
		.Do( (RenderViewDataDelegate)delegate(string viewName, string masterName, object viewData)
	{
		RenderViewData = new RenderViewData
		{
			ViewName = viewName, 
			MasterName = masterName, 	
			ViewData = viewData
		};
	});
	
	typeof(Controller).GetMethod("RedirectToAction", new []{typeof(RouteValueDictionary}})
		.Invoke(controller, new []{null});
	LastCall.IgnoreArguments().Repeat.Any()
		.Do( (RedirectToActionDelegate) delegate(RouteValueDictionary route)
	{
		RedirectToActionData = new RedirectToActionData
		{
			ActionName = Convert.ToString(values.ContainsKey("action") ? values["action"] : ""),
			ControllerName = Convert.ToString(values.ContainsKey("controller") ? values["controller"] : "")
		};
	});

	_mocks.Repaly(controller);
	
	return controller;
}

A lot of the ugliness here is the need to make calls with reflection, because they aren't exposed externally. Ignoring that, it is really a trivial exercise.

Just to point out, I added this functionality to Rhino Mocks a long time ago, specifically to enable the Extract and Override scenarios. This is a technique used for legacy code testing, it should be frowned upon in most green field scenarios.

Comments

Kyle Baley
03/19/2008 06:46 PM by
Kyle Baley

Oy! That's method number 4 for testing controllers and we still don't have a decent one yet. I'm one more workaround from giving up on testing controllers in ASP.NET MVC altogether until the next drop.

Ayende Rahien
03/19/2008 07:03 PM by
Ayende Rahien

Kyle,

No hacks needed to test MonoRail Controllers :-)

Vladan Strigo
03/20/2008 07:40 AM by
Vladan Strigo

I am having a feeling that ASP.NET MVC is quickly becoming a legacy application experience even before it comes out.

That's not a smell... that purely and simply stinks :(

--

Vladan

Will Shaver
03/20/2008 03:20 PM by
Will Shaver

Thanks for providing a better way to write that code. I'm fairly new to Rhino Mocks and my lack of experience apparently shows in the code I wrote for the TestHelper. That said, I'd be even happier if it were rendered obsolete in the next release of the MVC framework.

Comments have been closed on this topic.