I was working on a feature recently that began with a scenario in Specflow, something like this…
Scenario: New password min length
Given I have entered a password with only 5 characters
When I hit save
Then I see a validation error against password
So, first off I’m going to need to simulate a value being posted to an action method in the form collection. The simplest approach would be something like this…
[HttpPost]
public ActionResult ResetPasswordSubmit(string password)
{
}
However, it doesn’t really feel right to put the business rules around password validation directly in to this action method, as there may be other places in the application where a user can change their password. A better approach would seem to be implementing the validation rules as Data Annotation attributes against the model class. The method then becomes something like this…
public class SecurityController : Controller
{
private readonly ISecurityService securityService;
public SecurityController(ISecurityService service)
{
this.securityService = service;
}
[HttpPost]
public ActionResult ResetPasswordSubmit()
{
var user = this.securityService.GetSignedInUser();
if (this.TryUpdateModel(user, new string[] { "Password" }))
{
// save user
}
}
}
Working with a model class that looks a bit like…
using System.ComponentModel.DataAnnotations;
public class User
{
[StringLength(20, MinimumLength = 5)]
public string Password { get; set; }
}
So what would happen now is that the browser would post a single value in the form collection (Password). When TryUpdateModel is called, the framework will use a model binder to map the request information onto the model properties. The model binder to use can change according to the type being mapped, but in this case the DefaultModelBinder will be used.
The model binders need to make use of information from the request (such as posted form values, querystring etc). Value Providers provide an abstraction around this, and the framework ships with various providers to interrogate the different types of request information. In our example, the FormValueProvider would map the Password value from the form collection onto the Password property of our model class. As part of this process, the DefaultModelBinder will apply any validation rules set up via Data Annotation attributes, and add any validation errors into ModelState.
Time to get started with our unit test. With SpecFlow, the scenario is defined in a plain English language based on the Cucumber DSL. When you save the scenario, Specflow will generate a test fixture that includes a test method with the scenario name – in our case NewPasswordMinLength. The test method will attempt to call a method for each individual step in the scenario based on binding attributes applied to the methods.
We can start by adding a new item, and picking Specflow Step definition. To map the scenario steps in our example would like this…
[Binding]
public class ResetPasswordSteps
{
private ActionResult result;
private readonly Mock<ISecurityService>;
private SecurityController controller;
private User user;
[BeforeScenario]
public void BeforeScenario()
{
this.securityService = new Mock<ISecurityService>();
this.controller = new SecurityController(
this.securityService.Object);
this.user = new User { Id = 1 };
this.securityService
.Setup(s => s.GetSignedInUser())
.Returns(this.user);
}
[Given(@"I have entered a password with only 5 characters")]
public void GivenIHaveEnteredAPasswordWithOnly5Characters()
{
ScenarioContext.Current["Password"] = "Foot";
}
[When(@"I hit save")]
public void WhenIHitSave()
{
// todo: simulate the form value being posted
this.result = this.controller.ResetPassword();
}
[Then(@"I see a validation error against password")]
public void ThenISeeAValidationErrorAgainstPassword()
{
var view = this.result as ViewResult;
Assert.IsNotNull(view);
Assert.AreEqual("ResetPassword", view.ViewName);
Assert.IsFalse(view.ViewData.ModelState.IsValidField("Password");
}
}
First we set define a method decorated with the BeforeScenario attribute. This is a Specflow attribute that indicates the method should get executed before each scenario. This can be used to carry out any initialisation required. In this case, I’m mocking the SecurityService dependency and setting it up to return a dummy User object when the GetSignedInUser method gets called.
Executing the NUnit test would evaluate each step in the written scenario, discover these methods based on the binding attributes, and execute them in turn. Any state information that needs to be preserved can be either stored in private fields or on the ScenarioContext object.
The bit missing is how to simulate a form value being posted in the unit test. Remember the binders will use ValueProviders to get information from the request, so this is the piece we need to mock or simulate. To get this working, we would need to add the following code…
[When(@"I hit save")]
public void WhenIHitSave()
{
var form = new FormCollection
{
{ "Password", (string)ScenarionContext["Password"] }
};
this.controller.ValueProvider = form.ToValueProvider();
this.result = this.controller.ResetPasswordSubmit();
}
Here we are using the FormCollection class to simulate values being posted, and setting the controller to specifically use this value provider for binding scenarios, instead of the default set of providers.
At this point in time, we get a null reference exception running the test, as TryUpdateModel assumes that the ControllerContext property on the controller is available. To get the test working we need to mock this, so back in our initialisation method we add a few lines…
public void BeforeScenario()
{
this.controller = new SecurityController(this.securityService.Object);
var httpContextMock = new Mock<HttpContextBase>();
this.controller.ControllerContext = new ControllerContext(
httpContextMock.Object,
new RouteData(),
this.controller);
this.user = new User { Id = 1 };
this.securityService
.Setup(s => s.GetSignedInUser())
.Returns(this.user);
}
That’s all we need. To recap…
- We started by writing a plain English scenario in Specflow
- We created a class to contain the step definitions and added binding attributes to map the Specflow scenario steps to the right methods
- We added a method to run before the scenario to initialize the controller, mock the dependencies and provide a ControllerContext
- In the WhenIHitSave method, we initialized a FormCollection with simulated form values and set the ValueProvider on the controller to make use of this
- In our ‘Then’ method we assert that the view was returned with a ModelState error against the Password field
- The end result is an executable acceptance test (by default this will be NUnit unless Specflow is configured otherwise)
All mocking examples have used Moq which is awesome.
