Joe Buschmann

let topics = [csharp; specflow; fun]

Some SpecFlow Tips

UPDATE (11/18/2016):

I've written a number of posts since this one was published that cover advanced Specflow topics like composable steps, tags done right, managing state, useful regex, etc. For a list of all my Specflow writing, you can click on the Specflow tag or go here: http://joebuschmann.com/tag/specflow. Specflow is a fantastic tool that's changed the way I develop software, and I hope it does the same for you. Happy testing!


Late last year a co-worker Jay Brummels introduced our development group to SpecFlow a .NET tool that enables behavior specifications from a domain expert or product owner to be bound to runnable test code. It has been a huge success, and we're quickly building up a suite of system tests and unit tests fronted by Specflow.

Although we're relatively new to SpecFlow, my group has identified some patterns or best practices to follow when building out tests.

Use Extension Methods to Manipulate ScenarioContext.Current

ScenarioContext.Current is a singleton that provides contextual information about the test and executing scenario block as well as a dictionary data structure for storing test data between scenario blocks. Below is an example of retrieving an object, manipulating it, and stashing it back in ScenarioContext dictionary.

// Direct access to the dictionary in ScenarioContext.Current.  
Customer customer = ScenarioContext.Current.Get<Customer>("CurrentCustomer");

ManipulateCustomer(customer);

Validate(customer);

ScenarioContext.Current.Set(customer, "CurrentCustomer");

Using the dictionary directly from step code has some disadvantages. First access to the data requires passing a string key which is not discoverable via Intellisense. Second creating an object may require multiple dictionary items to build it out. Having that logic in multiple places is prone to error.

To solve these issues, extension methods can be used to store and retrieve test data.

public static class CustomerScenarioContextExtensions  
{
    public static Customer Customer(this ScenarioContext context)
    {
        return context.Get<Customer>("CurrentCustomer");
    }

    public static void Customer(this ScenarioContext context, Customer customer)
    {
        context.Set(customer, "CurrentCustomer");
    }
}
// Use extension methods instead.  
Customer customer = ScenarioContext.Current.Customer();

ManipulateCustomer(customer);

Validate(customer);

ScenarioContext.Current.Customer(customer);

And to take things one step further, the creation of a complex object using multiple entries in the dictionary can be hidden behind an extension method.

public static SaveCustomerRequest CreateSaveCustomerRequest(  
    this ScenarioContext context)
{
    SaveCustomerRequest request = new SaveCustomerRequest();
    request.SecurityToken = context.Get<string>("SecurityToken");
    request.SessionId = context.Get<string>("SessionId");
    request.Customer = context.Customer();

    return request;
}

Bind Multiple Specification Attributes

After writing out a step specification, step definitions need to be generated and filled in with the implementation code. Whenever I notice the same step definition being used from different scenario blocks, I like to consolidate them into one method and add multiple step definition attributes. Also, I like to rename the method to something more concise. By default Specflow generates method names that match the step specification text, and they tend to be long and wordy.

// Step definitions with the default signatures created by Specflow.  
[Given(@"a customer with the name (.*) (.*)")]
public void GivenACustomerWithTheName(string firstName, string lastName)  
{
    Customer customer = new Customer();
    customer.FirstName = firstName;
    customer.LastName = lastName;

    ScenarioContext.Current.Customer(customer);
}

[When(@"I create a customer named (.*) (.*)")]
public void WhenICreateACustomerNamed(string firstName, string lastName)  
{
    Customer customer = new Customer();
    customer.FirstName = firstName;
    customer.LastName = lastName;

    ScenarioContext.Current.Customer(customer);            
}
// A consolidated step definition with multiple step definition attributes.  
[Given(@"a customer with the name (.*) (.*)")]
[When(@"I create a customer named (.*) (.*)")]
public void CreateCustomer(string firstName, string lastName)  
{
    Customer customer = new Customer();
    customer.FirstName = firstName;
    customer.LastName = lastName;

    ScenarioContext.Current.Customer(customer);
}

Use Hooks to Execute Setup or Teardown Code

As with any other testing framework, SpecFlow provides a way of executing setup or teardown code for features, scenarios, steps, and test runs via hook classes. Hooks are associated to tests using tags which are keywords preceded by an @ symbol.

At work my development group has created a series of system tests for an ordering system. Each test places an actual order in an integrated testing environment. After a test run, dozens of orders remain in the system taking up valuable space and resources. To solve this problem, a hook runs after each ordering scenario to cancel the order and remove it from the system.

[Binding]  
public class CancelOrderHook  
{
    [AfterScenario("CancelOrder")]
    public void AfterScenario()
    {
        AbstractOrderData order = ScenarioContext.Current.CurrentOrder();

        if (order == null)
            throw new MissingOrderException();

        var input = new CancelOrderCommandInput
            {
                OrderId = order.Id,
            };

        using (var svc = CreateServiceProxy()
        {
            var request = new ExecuteCommandRequest(input);
            var response = svc.Channel.ExecuteCommand(request);
            response.AssertSuccessfulCommandExecution();
        }
    }
}

Tests scenarios that create orders can use the @CancelOrder tag to run the hook after the scenario completes.

@CancelOrder  
Scenario: Successfully place an order and verify the shopping cart  
    Given an open order with product1
    When I complete the order
    Then the following items should be in the shopping cart
     | Name      |
     | Product 1 |