BDD with SpecFlow, NUnit and MVC3–Refactored
Whilst the previous post (BDD with SpecFlow, NUnit and MVC3–More View a List Of Products) got the job done, it could have done it a lot better.
After a conversation on Twitter and in the comments on the previous post with Darren Cauthon (@darrencauthon, http://www.cauthon.com) and Marcus Hammarberg (@marcusoftnet, http://www.marcusoft.net), there are three areas where I can refactor to improve the tests:
1 Language
In an earlier post I was torn between ‘honest’ language and non-technical language:
Then comes the first scenario, ‘Display the default products view’, and where I run into naming trouble. I decided that I would start testing at the controller rather than the interface so, as much as I would like to write out the scenario as:
Scenario: Display the default products view
When I navigate to /product/
Then the products page should be displayed
And the page should contain a list of productsThat sounds misleading as I am not navigating to a URL or displaying a page. I’ll use a more honest description:
Scenario: Display the default products view
When the product controller is told to display the default view
Then the controller should return a view
And the view title should be products
And the view should contain a list of productsThis better describes what I want to do but is less easily understood by any non technical stakeholder (which is one of the benefits of BDD). I think it is probably better than the alternatives in my case which are either testing the GUI or a misleading scenario.
I have now swung to the non-technical viewpoint and rewritten my scenario to:
Scenario: Visit the product pageWhen I visit the product pageThen I should see the Index viewAnd the view title should be ProductsAnd the view should contain a list of products
It’s then just a case of replacing the tags on the steps and, because I’m a neat freak, renaming the methods to match the tags.
2 State in Class Fields
Storing the controller and the result as private fields in the Step class is fine until you want to test a second controller. I added a scenario for testing a Tenant controller:
Scenario: Visit the tenant pageWhen I visit the tenant pageThen I should see the Index view
I added a new TenantControllerSteps file and wrote the ‘I visit the tenant page’ step, compiled and ran the tests and got a failure. The reason for this is that the test picked up the ‘Then I should see the Index view’ from the ProductControllerSteps which is great for reuse, not so great that the method used the _result field from the ProductControllerSteps which is of course null at that point. For a demonstration of this, check out Darren’s video: SpecFlow Anti-Pattern: Using Private Members to Retain State Between Steps. The answer is to store state in ScenarioContext.Current.
The refactored code is now in two class files, steps specific to ProductController
[Binding]public class ProductControllerSteps {[Given(@"There are (.*) products")]public void GivenThereAreXProducts(int productCount) {ProductController controller = CreateProductController(productCount);ScenarioContext.Current.Set(controller);}[When(@"I visit the product page")]public void VisitTheProductPage() {var controller = ScenarioContext.Current.Get<ProductController>();ActionResult result = controller.Index();ScenarioContext.Current.Set(result);}[Then(@"the view should contain a list of products")]public void ThenTheViewShouldContainAListOfProducts() {var viewResult = (ViewResult) ScenarioContext.Current.Get<ActionResult>();viewResult.ViewData.Model.ShouldBeType(typeof (List<Product>));}[Then(@"the view should contain a list of (.*) products")]public void ThenTheViewShouldContainAListOfXProducts(int productCount) {var viewResult = (ViewResult) ScenarioContext.Current.Get<ActionResult>();var products = (List<Product>) viewResult.ViewData.Model;products.Count.ShouldEqual(productCount);}private static ProductController CreateProductController(int productCount) {var context = new FakeRavenContext();context.AddProducts(productCount);return new ProductController(new ProductRepository(context));}public ProductControllerSteps() {ProductController controller = CreateProductController(0);ScenarioContext.Current.Set(controller);}}
And steps that can be reused across different controllers
[Binding]public class ControllerSteps {[Then(@"I should see the (.*) view")]public void ThenIShouldSeeTheView(string viewName) {var result = ScenarioContext.Current.Get<ActionResult>();result.ShouldBeType(typeof (ViewResult));var viewResult = (ViewResult) ScenarioContext.Current.Get<ActionResult>();viewResult.ViewName.ShouldEqual(viewName);}[Then(@"the view title should be (.*)")]public void ThenTheViewTitleShouldBeProducts(string viewTitle) {var viewResult = (ViewResult) ScenarioContext.Current.Get<ActionResult>();viewResult.ViewData["Title"].ShouldEqual(viewTitle);}}
3 Use Tables to Specify Entities
In the code above, I am checking that the correct number of products are displayed, I am not testing they are the correct products though, this will become more important when I start adding and editing products. Darren has a library on Github to help with using tables in SpecFlow and there is a video on TekPub in which he explains it.
The refactored feature now looks like this:
Feature: View a list of productsIn order to see what products there areAs a userI want to view a list of productsScenario: Visit the product pageWhen I visit the product pageThen I should see the Index viewAnd the view title should be ProductsAnd the view should contain a list of productsScenario: Display 10 productsGiven There are these products|Id |Active |Name |Reference ||1 |true |Product 1 |Ref-1 ||2 |true |Product 2 |Ref-2 ||3 |true |Product 3 |Ref-3 ||4 |true |Product 4 |Ref-4 ||5 |true |Product 5 |Ref-5 ||6 |false |Product 6 |Ref-6 ||7 |false |Product 7 |Ref-7 ||8 |false |Product 8 |Ref-8 ||9 |false |Product 9 |Ref-9 ||10 |false |Product 10 |Ref-10 |When I visit the product pageThen the view should contain a list of these products|Id |Active |Name |Reference ||1 |true |Product 1 |Ref-1 ||2 |true |Product 2 |Ref-2 ||3 |true |Product 3 |Ref-3 ||4 |true |Product 4 |Ref-4 ||5 |true |Product 5 |Ref-5 ||6 |false |Product 6 |Ref-6 ||7 |false |Product 7 |Ref-7 ||8 |false |Product 8 |Ref-8 ||9 |false |Product 9 |Ref-9 ||10 |false |Product 10 |Ref-10 |Scenario: Display 0 productsGiven There are these products|Id |Active |Name |Reference |When I visit the product pageThen the view should contain a list of these products|Id |Active |Name |Reference |
And the two steps that have been amended are now:
[Given(@"There are these products")]public void GivenThereAreTheseProducts(Table table) {var products = table.CreateSet<Product>();var context = new FakeRavenContext {Products = new FakeDbSet<Product>(products)};var controller = new ProductController(new ProductRepository(context));ScenarioContext.Current.Set(controller);}[Then(@"the view should contain a list of these products")]public void ThenTheViewShouldContainAListOfTheseProducts(Table table) {var viewResult = (ViewResult) ScenarioContext.Current.Get<ActionResult>();var products = (List<Product>) viewResult.ViewData.Model;table.CompareToSet(products);}
I was expecting tables to be a pain but Darren’s Assist library really makes using tables easy in SpecFlow.