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 page When I visit the product page Then I should see the Index view And the view title should be Products And 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 page When I visit the tenant page Then 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 products In order to see what products there are As a user I want to view a list of products Scenario: Visit the product page When I visit the product page Then I should see the Index view And the view title should be Products And the view should contain a list of products Scenario: Display 10 products Given 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 page Then 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 products Given There are these products |Id |Active |Name |Reference | When I visit the product page Then 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.