We all know that you shouldn't test private methods, but the same can't be said about protected methods. Look at this slightly modified version of the UserBinder from the CodeBetter.Canvas
project:
public abstract class ModelBinder<T> : IModelBinder where T : class
{
//..all types of code
public object BindModel(NameValueCollection parameters, Action<string> addErrors)
{
var id = Get("id").ToInt(0);
T entity = null;
if (id == 0)
{
entity = BindNew();
}
else
{
entity = _repository.Find<T>(id);
BindExisting(entity);
}
if (entity == null)
{
AddError("Something bad happened");
return;
}
var errors = Validate(entity);
if (errors != null)
{
errors.Each(addErrors);
}
return entity;
}
protected abstract T BindNew();
protected abstract void BindExisting(T original);
protected virtual string[] Validate(T entity)
{
return null;
}
}
We are binding a new or existing user to our submitted data (that's what BindExisting and BindExisting do), and assuming that worked, calling Validate. Its up to each binder to implement the BindNew, BindExisting and Validate methods (the last one being optional). Our UserBinder looks something like:
public class UserBinder : ModelBinder<User>
{
protected override User BindNew()
{
var user = new User();
BindCommon(user);
return user;
}
protected override void BindExisting(User user)
{
BindCommon(user);
}
private void BindCommon(User user)
{
//Parameters is defined in the base binder, it just wraps Request.Form
user.Name = Parameters("Name");
user.Email = Parameters("Email");
return user;
}
protected override string[] Validate(User user)
{
if (user.Name == "Karl")
{
return new[] {"there can be only one"};
}
return null;
}
}
So far everything's great, our base ModelBinder class provides a generic binding framework, and subclasses specify the binding implementation for individual entities.
However, when we go to unit test our UserBinder's Validate method, we can't access it because its marked as protected. A simple solution might be to change it to public, or protected internal (and use the ugly InternalsVisibleTo attribute). I'm a big believer that real code should never be modified for the purpose of unit testing (including marking methods as virtual (I believe in virtual by default, but not because of unit testing)). Frankly, if you are having to change production code to make it testable, then you are doing something wrong.
The right solution (or at least the solution I've been using), is to simply subclass the UserBinder within your test project:
public class TestUserBinder : UserBinder
{
public string[] PublicValidate(User user)
{
//forward the call to the base implementation
return Validate(user);
}
}
We can now write a straightforward test:
[Fact]
public void ReturnsErrorWhenNameIsReserved()
{
var binder = new TestUserBinder();
var errors = binder.PublicValidate(new User{ Name = "Karl"});
Assert.Equal(1, errors.Length);
Assert.Equal("there can be only one", errors[0]);
}
Posted
Wed, Aug 19 2009 1:03 PM
by
karl