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 products

That 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 products

This 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.

Leave a Reply