I’m on a quest to learn BDD. Thanks for joining me. Hopefully we can learn some good principles along the way. I know deep down that test-first development produces better code. I believe that Behavior-Driven Development is a more intuitive way to express what Test-Driven Development proponents have been preaching for while. Machine.Specifications (mspec for short) is a testing framework that makes BDD a little easier when developing .NET applications. So, this three part series is really just me talking through the process of creating tests/specs while focusing on an MVC concept that I needed to learn anyways: the ModelBinder.
The toughest part of TDD/BDD so far has been discipline. It’s SO easy to want to write code for this component or that class outside the context of the test. It’s a challenge for me to maintain the discipline it takes to write a spec to fail and write code to pass the test. But I’m getting the hang of it. Failure and success, and only a bit of code to separate the two extremes.
In part 1, I created a spec using mspec and got it to the point where it would fail miserably. It’s amazing how much work can go into failure.
Now, the task at hand is to make the test pass! Here’s the spec:
public class when_binding_guestbook_post_data_to_viewmodel
{
static TestModelBinder _modelBinder;
static ModelBindingContext _bindingContext;
static object _result;
Establish context = () =>
{
var nameValueCollection = new NameValueCollection
{
{ "Name", "Scott Hanselman" },
{ "Phone", "776-555-1212" }
};
_bindingContext = new ModelBindingContext
{
ModelName = "GuestBookViewModel",
ValueProvider = new FormCollection(nameValueCollection)
};
_modelBinder = new TestModelBinder();
};
Because of = () => _result = _modelBinder.BindModel(null, _bindingContext);
It should_not_be_null = () => _result.ShouldNotBeNull();
}
According to our spec, “when binding guestbook post data to viewmodel, it (the ViewModel) should not be null.” Here’s the ModelBinder class that we’re targetting with our spec:
public class TestModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
throw new NotImplementedException();
}
}
Passing Spec #1
The test fails because the method throws a NotImplementedException. If all I have to do is make sure the ModelBinder returns something other than null… well, that should be easy enough:
public class TestModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
return "Hello World";
}
}
… and now the test passes!
Passing Spec #2
Of course, “Hello World” is FAR from what we really want to be returned from our ModelBinder. So, let’s add another criteria to our spec (from here on out, I’ll spare you the complete re-paste of the spec):
It should_be_a_guestbook_view_model = () => _result.ShouldBeOfType<GuestBookViewModel>();
So, now, we’re saying “when binding guestbook post data to a ViewModel, it (the ViewModel) should be a GuestBookViewModel.” When writing this spec, I let ReSharper help me turn my imaginary GuestBookViewModel into a real live stubbed out class:
public class GuestBookViewModel
{
}
Its kind of obvious at this point, but, as a discipline, I’m going to run the spec and make sure it fails. It does. Moving on.
Now, let’s make it pass. To do so, we need to go to our TestModelBinder class and poke around. In order to make the test pass, we just have it return the proper model:
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
return new GuestBookViewModel();
}
Run the test and… it passes!
Passing Spec #3
It should_have_the_correct_name = () =>
((GuestBookViewModel)_result).Name.ShouldEqual("Scott Hanselman");
Now we’re saying the viewmodel, “should have the correct name” (i.e. Scott Hanselman). When I added this spec, I let ReSharper help me create a new property in my GuestBookViewModel class called “Name”. So, now my ViewModel looks like:
public class GuestBookViewModel
{
public string Name { get; set; }
}
Being disciplined, I first verify that my test fails, which it does. The next step is to make it pass. To do that, we need to go back to the ModelBinder and make some adjustments. Now, I could keep going down this road of hard-coding return values, but it won’t end well. So, I’m going to up the ante a little bit and ACTUALLY use the model binder to make the test pass.
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var vm = new GuestBookViewModel();
vm.Name = bindingContext.ValueProvider.GetValue("Name").AttemptedValue;
return vm;
}
Test and pass.
Passing Spec #4
This one should be easy…
It should_have_the_correct_phone_number = () =>
((GuestBookViewModel)_result).Phone.ShouldEqual("776-555-1212");
Added a phone number property to our ViewModel class.
public class GuestBookViewModel
{
public string Name { get; set; }
public string Phone { get; set; }
}
Test and fail. Hit the ModelBinder and adjust to make it pass.
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var vm = new GuestBookViewModel();
vm.Name = bindingContext.ValueProvider.GetValue("Name").AttemptedValue;
vm.Phone = bindingContext.ValueProvider.GetValue("Phone").AttemptedValue;
return vm;
}
Test and pass.
To take it a step further, you should check out the 3rd and last installment in this series.