In my last post on unit testing, I had written about a technique I’d learnt forhttp://niftybits.wordpress.com/2013/08/14/unit-tests-simplifying-test-setup-with-builders/[simplifying 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 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: :(
///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.