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.
Hi John,
Thanks for writing about SpecFlow and MVC3! I have a coupon comments to make:
1.) Have you considered using ScenarioContext instead of those private members to your step definintion class? I recorded a video on the subject here: http://www.youtube.com/watch?v=IGvxMPX55vE
2.) I think there’s an amount of context and business definition that’s being lost in your step definitions, most noticeably in the given and then clauses. For example, when you say
“There are 10 products”
What does that mean? Yeah, ten products, but what are the products? This type of definition is sometimes (though rarely) necessary, but another way to consider doing it is to say:
Given I have the following products:
| ProductId | Name |
| 1 | Test Product 1 |
| 2 | Test Product 2 |
| 3 | Test Product 3 |
…
and so on. That will actually give you a greater amount of substance to what’s relevant to the business, and will give you a better platform to write more substantial clauses later. Like your then clause:
“the view should contain a list of (.*) products”
What products? The ones that I passed in? What if I returned back ten products that I new’d up in the controller? What if there was a situation where I meant to pass in 10 and I wanted 5 back?
Another way to try this is to say:
Then the user should see the following products:
| ProductId | Name |
| 1 | Test Product 1 |
| 2 | Test Product 2 |
| 3 | Test Product 3 |
…
There are helpers in the SpecFlow.Assist namespace that will assist you in making those definitions work.
3.) This is more of a style matter, but terms like “controller” and “view” and anything else that is MVC-related has absolutely *NO* business relevance. It’s programmer talk. This is just a suggestion, but I’d try writing those business specs from the perspective of a non-programmer user. To them, they go to a page and see stuff. They don’t call actions on controllers and look at views. 🙂
Anyway, I hope that helps!
Darren
thanks Darren,();}}
Marcus had just about convinced me to switch to the business language and I had hit the problem of storing state in private fields.
Any thoughts on putting the ScenarioContext inside a property e.g.
private ProductController Controller{get{return ScenarioContext.Current.Get
I’ll write another post, refactoring with your suggestions. Great work with SpecFlow.
John,
I think making a property for the product controller might be overkill, since there’s already a method on ScenarioContext to do essentially that. If necessary, you can do this:
ScenarioContext.Current.Set(new ProductController());
ScenarioContext.Current.Get();
I’d never use a singleton like ScenarioContext.Current in live production code, but given the constraints of the language and the “flatness” that SpecFlow/Cucumber steps work, it’s one of the few places where it works. Setting up architecture around it is a battle you’ll never win. 🙂
Darren
In case it helps, some of the helper methods I mentioned earlier are on this Tekpub video:
http://tekpub.com/view/dotnet-oss/dotnetoss_specflowassist2
Really appreciate the work done here – great to see the result of give ‘n take and the evolution of code. (fwiw, the tekpub link to the helper methods is busted.)
thx!
Thanks Steve,
It looks as though Rob Connery may have changed his routing. The video on Tekpub is here now:
http://tekpub.com/view/dotnet-oss/6
I think this is a really practical and useful post. I like your approach and your conclusions.
One thing that I disagree with however is the idea that any of these bdd tests would reduce the number of unit tests you should be writing.
BDD is about working with others to make sure that your software satisfies a business need, even when those needs change. Unit testing is there to make sure that all the every chunk of code you write does what it’s suppose to.
There’s some pretty serious implications to using acceptance tests to validate the correctness of all your code. It basically doesn’t work.
J.B. Rainsberger explains this fairly well in this video:
http://www.infoq.com/news/2009/04/jbrains-integration-test-scam
Woops, wrong link.
Video:
http://www.infoq.com/presentations/integration-tests-scam
i have jsonresult in my project how to do bdd with that