In my last post on unit testing, I had written about a technique I’d learnt forsimplifying test set ups with the builder pattern. It provides a higher level, more readable API resulting in DAMP tests.
Implementing it though presented a few interesting issues that were fun to solve and hopefully, instructive as well. I for one will need to look it up if I spend a few months doing something else – so got to write it down :).
In Scheduler user portal, controllers derive from the MVC4 Controller
class whereas others derive from a custom base Controller
. For instance, Controllers that deal with logged in interactions derive from TenantController
which provides TenantId
and SubscriptionId
properties. IOW, a pretty ordinary and commonplace setup.
class EventsController : Controller { public ActionResult Post (MyModel model) { // access request, form and other http things } } class TenantController: Controller { public Guid TenantId {get; set;} public Guid SubscriptionId {get; set;} } class TaskController: TenantController { public ActionResult GetTasks() { // Http things and most probably tenantId and subId as well. } }
So, tests for EventsController
will require HTTP setup (request content, headers etc) where as for anything deriving from TenantController
we also need to be able to set up things like TenantId
.
Builder API
Let’s start from how we’d like our API to be. So, for something that just requires HTTP context, we’d like to say:
controller = new EventsControllerBuilder() .WithConstructorParams(mockOpsRepo.Object) .Build();
And for something that derives from TenantController
:
controller = new TaskControllerBuilder() .WithConstructorParams(mockOpsRepo.Object) .WithTenantId(theTenantId) .WithSubscriptionId(theSubId) .Build();
The controller builder will basically keep track of the different options and always return this
to facilitate chaining. Apart from that, it has a Build
method which builds a Controller
object according to the different options and then returns the controller. Something like this:
class TaskControllerBuilder() { private object[] args; private Guid tenantId; public TaskControllerBuilder WithConstructorParams(params object args ) { this.args = args; return this; } public TaskControllerBuilder WithTenantId(Guid id ) { this.tenantId = id; return this; } public TaskController Build() { var mock = new Mock<TaskController>(MockBehavior.Strict, args); mock.Setup(t => t.TenantId).Returns(tenantId); return mock.Object; } }
Generics
Writing XXXControllerBuilder
for every controller isn’t even funny – that’s where generics come in – so something like this might be easier:
controller = new ControllerBuilder<EventsController>() .WithConstructorParams(mockOpsRepo.Object) .Build();
and the generic class as:
class ControllerBuilder<T>() where T: Controller { private object[] args; private Guid tenantId; protected Mock<T> mockController; public ControllerBuilder<T> WithConstructorParams(params object[] args ) { this.args = args; return this; } public T Build() { mockController = new Mock<T>(MockBehavior.Strict, args); mockController.Setup(t => t.TenantId).Returns(tenantId); return mock.Object; } }
In takes about 2 seconds to realize that it won’t work – since the constraint only specifies T should be a subclass of Controller
, we do not have the TenantId or SubscriptionId properties in the Build
method.
Hmm – so a little refactoring is in order. A base ControllerBuilder
that can be used for only plain
controllers and a sub class for controllers deriving from TenantController
. So lets move the tenantId
out of the way from ControllerBuilder
.
class TenantControllerBuilder<T>: ControllerBuilder<T> where T: TenantController // and this will allow to // access TenantId and SubscriptionId { private Guid tenantId; public TenantControllerBuilder<T> WithTenantId(Guid tenantId) { this.tenatId = tenantId; return this; } public T Build() { // call the base var mock = base.Build(); // do additional stuff specific to TenantController sub classes. mockController.Setup(t => t.TenantId).Returns(this.tenantId); return mock.Object; } }
Now, this will work as intended:
/// This will work: controller = new TenantControllerBuilder<TaskController>() .WithTenantId(guid) // Returns TenantControllerBuilder<T> .WithConstructorParams(mockOpsRepo.Object) // okay! .Build();
But this won’t compile: 😦
controller = new TenantControllerBuilder<TaskController>() .WithConstructorParams(mockOpsRepo.Object) // returns ControllerBuilder<T> .WithTenantId(guid) // Compiler can't resolve WithTenant method. .Build();
This is basically return type covariance and its not supported in C# and will likely never be. With good reason too – if the base class contract says that you’ll get a ControllerBuilder
, then the derived class cannot provide a stricter contract that it will provide not only a ControllerBuilder
but that it will only be TenantControllerBuilder
.
But this does muck up our builder API’s chainability – telling clients to call methods in certain arbitrary sequence is a no – no. And this is where extensions provide a neat solution. Its in two parts
- Keep only state in
TenantControllerBuilder
. - Use an extension class to convert from
ControllerBuilder
toTenantControllerBuilder
safely with the extension api.
// Only state: class TenantControllerBuilder<T> : ControllerBuilder<T> where T : TenantController { public Guid TenantId { get; set; } public override T Build() { var mock = base.Build(); this.mockController.SetupGet(t => t.TenantId).Returns(this.TenantId); return mock; } } // And extensions that restore chainability static class TenantControllerBuilderExtensions { public static TenantControllerBuilder<T> WithTenantId<T>( this ControllerBuilder<T> t, Guid guid) where T : TenantController { TenantControllerBuilder<T> c = (TenantControllerBuilder<T>)t; c.TenantId = guid; return c; } public static TenantControllerBuilder<T> WithoutTenant<T>(this ControllerBuilder<T> t) where T : TenantController { TenantControllerBuilder<T> c = (TenantControllerBuilder<T>)t; c.TenantId = Guid.Empty; return c; } }
So, going back to our API:
///This now works as intended controller = new TenantControllerBuilder<TaskController>() .WithConstructorParams(mockOpsRepo.Object) // returns ControllerBuilder<T> .WithTenantId(guid) // Resolves to the extension method .Build();
It’s nice sometimes to have your cake and eat it too :D.