BDD with SpecFlow, NUnit and MVC3–More View a List Of Products
Following on from this post where it looked like BDD was a spectacularly inefficient way to drive out code, I’ll add a new feature to the scenario:
Scenario: Display 10 products
Given There are 10 products
When the product controller is told to display the default view
Then the view should contain a list of 10 products
Compiling and running the tests provides templates for the Given and the Then methods, we get to reuse the existing When method. This reuse is one of the big daws for BDD for me (plus the readability).
To write the method GivenThereAre10Products I need to tell the controller there are ten products. The simplest thing to do is to pass 10 as an argument to the controller constructor then write a loop to populate the product list. I need to move the controller out of WhenTheProductControllerIsToldToDisplayTheDefaultView and declare it as a class field, then re new it in GivenThereAre10Products:
private ProductController controller = new ProductController(0);[Given(@"There are 10 products")]public void GivenThereAre10Products() {controller = new ProductController(10);}[When(@"the product controller is told to display the default view")]public void WhenTheProductControllerIsToldToDisplayTheDefaultView() {_result = controller.Index();}[Then(@"the view should contain a list of 10 products")]public void ThenTheViewShouldContainAListOf10Products() {var products = (List<Product>) ViewResult.ViewData.Model;products.Count.ShouldEqual(10);}
I can then write the code to make these tests compile then pass, the relevant changes to the controller are:
private readonly int _productsCount;public ProductController(int productsCount) {_productsCount = productsCount;}public ActionResult Index() {ViewData["Title"] = "Products";var products = new List<Product>();var product = new Product();for (int i = 0; i < _productsCount; i++) {products.Add(product);}return View(products);}
This makes the tests pass but needs refactoring. As is fairly standard practise I will refactor to pass a repository into the controller and the repository will be passed a context which will, in this instance fake the database connection. I will detail the repository and fake context in a separate post, but I include a method on the fake repository to add a number of an entity to help testing.
I need to refactor the specs to create a controller with a repository and the relevant refactored code is:
private ProductController _controller;private ActionResult _result;public ViewAListOfProducts() {CreateController(0);}private void CreateController(int productCount) {var context = new FakeRavenContext();context.AddProducts(productCount);_controller = new ProductController(new ProductRepository(context));}[Given(@"There are 10 products")]public void GivenThereAre10Products() {CreateController(10);}
And the refactored code from the controller:
private readonly IProductRepository _repository;public ProductController(IProductRepository repository) {_repository = repository;}public ActionResult Index() {ViewData["Title"] = "Products";return View(_repository.All());}
The final thing I want to do here is to add a scenario for 0 products:
Scenario: Display 0 products
Given There are 0 products
When the product controller is told to display the default view
Then the view should contain a list of 0 products
This is where SpecFlow begins to shine, I just need to parameterise 2 of the methods to implement this:
[Given(@"There are 10 products")]public void GivenThereAre10Products() {CreateController(10);}[Then(@"the view should contain a list of 10 products")]public void ThenTheViewShouldContainAListOf10Products() {var products = (List<Product>) ViewResult.ViewData.Model;products.Count.ShouldEqual(10);}
Becomes:
[Given(@"There are (.*) products")]public void GivenThereAreXProducts(int productCount) {CreateController(productCount);}[Then(@"the view should contain a list of (.*) products")]public void ThenTheViewShouldContainAListOfXProducts(int productCount) {var products = (List<Product>) ViewResult.ViewData.Model;products.Count.ShouldEqual(productCount);}
I still need to implement paging for this scenario then create the view details, add, edit, and delete product scenarios which should drive the design of the product class but I think the BDD side will be pretty much more of the same.
I expect coding with BDD will be at least as fast as TDD but I suspect I will still write some unit tests to complement the BDD specs.