Moving Beyond the Basics

Presented by Joe Buschmann

What is SpecFlow?

Define, manage, and automate human-readable acceptance tests in .NET

Enable BDD with easy to understand tests

Build up a living documentation of your system

Visual Studio Extension

  • Gherkin syntax highlighting
  • File templates for features, hooks, and step definitions
  • Executable test generation (code-behind)

NuGet Package

  • Test scenario context
  • Simple dependency injection
  • Table helper extension methods
Getting Started in Three Steps
Step 1: Specify
						
Feature: GoogleSearch
  In order to find information on the internet
  As a user of the Google search engine
  I want to perform a search

Scenario: Perform a Google Search
  Given a browser loaded with Google's web page
  When I search for "kittens"
  Then Google should return valid search results
						
					
Step 2: Bind
						
    [Binding]
    public class GoogleSearch
    {
        public GoogleSearch() { // Snip }

        [Given(@"a browser loaded with Google's web page")]
        public void LoadWebPage() { // Snip }

        [When(@"I search for (.*)")]
        public void Search(string searchTerm) { // Snip }

        [Then(@"Google should return valid search results")]
        public void ValidateSearchResults() { // Snip }
    }
    					
					
Step 3: Run
						
nunit-console.exe /xml:results.xml some\path\tests.dll
						
					

So you’ve written some tests using Specflow, and you’re feeling good about test coverage. But as the code base gets larger, some issues are starting to emerge.

Gherkin is difficult to bind

Fragile bindings

Duplicate code/lack of reusability

Tedious table manipulation

Unused and missing bindings

What can you do to fix these issues?

Reusable Bindings

Problems with Inheritance
TechTalk.SpecFlow.BindingException : Ambiguous step definitions found
for step 'Given Invoke Service'
					
SpecFlow supports a simple IoC framework

Inject dependencies with constructor parameters

						
[Binding]
public class Search
{
    private readonly IWebDriver _webDriver;
    private readonly ISearchProvider _searchProvider;

    public Search(IWebDriver webDriver, ISearchProvider searchProvider)
    {
        _webDriver = webDriver;
        _searchProvider = searchProvider;
    }

    // Snip
}
						
					

What can you get from the container?

  • Classes decorated with the Binding attribute
  • Custom context classes
  • Built-in context classes - e.g. ScenarioContext
  • The container itself - IObjectContainer
  • Dependencies explicitly registered with the container
Example 1: Retrieve the SpecFlow context objects
						
private readonly ScenarioContext _scenarioContext;

public AddressSteps(ScenarioContext scenarioContext)
{
    _scenarioContext = scenarioContext;
}
						
					
Example 2: You can use DI to load the appropriate Selenium web driver
						
[Binding]
public class BootstrapSelenium : IDisposable
{
    private readonly IObjectContainer _objectContainer;
    private IWebDriver _webDriver = null;

    public BootstrapSelenium(IObjectContainer objectContainer)
    {
        _objectContainer = objectContainer;
    }

    [BeforeScenario]
    public void LoadDriver()
    {
        _webDriver = BuildWebDriver();
        _objectContainer.RegisterInstanceAs(_webDriver, typeof (IWebDriver));
    }

    // Dispose will be called after scenario execution is complete
    public void Dispose()
    {
        _webDriver?.Quit();
    }
}
						
					
Example 3: You can use tags to conditionally load dependencies
						
@json
Scenario: Invoke service with JSON payload
  Given a fully populated request object
  When I invoke the service
  Then a valid response should be returned
						
					
						
[BeforeScenario("xml")]
public void ConfigureXml()
{
    _objectContainer
        .RegisterTypeAs<XmlBodySerializer, IBodySerializer>();
}

[BeforeScenario("json")]
public void ConfigureJson()
{
    _objectContainer
        .RegisterTypeAs<JsonBodySerializer, IBodySerializer>();
}

[BeforeScenario("formurlencoded")]
public void ConfigureFormUrlEncoded()
{
    _objectContainer
        .RegisterTypeAs<FormUrlEncodingBodySerializer, IBodySerializer>();
}
						
					
						
[Binding]
public sealed class AddressServiceSteps
{
    private readonly IBodySerializer _bodySerializer;

    public ContentTypeSteps(IBodySerializer bodySerializer)
    {
        _bodySerializer = bodySerializer;
    }

    // Snip
}
						
					
What about the Steps class?
						
public abstract class Steps
{
    // Built-in context
    public ScenarioContext ScenarioContext { get; }
    public FeatureContext FeatureContext { get; }
    public TestThreadContext TestThreadContext { get; }
    public ScenarioStepContext StepContext { get; }

    // Call other binding steps
    public void Given(string step);
    public void Given(string step, Table tableArg);
    public void Given(string step, string multilineTextArg);
    public void Given(string step, string multilineTextArg, Table tableArg);

    // Snip
}
						
					
						
Given the customer
  | Salutation | First Name | Last Name |
  | Miss       | Liz        | Lemon     |
And the address
  | Line 1                | City     | State | Zipcode |
  | 30 Rockefeller Plaza  | New York | NY    | 10112   |
						
					
						
[Given("a new customer and address")]
public void GivenANewCustomerAndAddress()
{
    Table customer = new Table("FirstName", "LastName", "Salutation");
    customer.AddRow("Miss" "Liz", "Lemon");

    Table address = new Table("Line 1", "City", "State", "Zipcode");
    address.AddRow("30 Rockefeller Plaza", "New York", "NY", "10112");

    Given("the customer", customer);
    Given("the address", address);
}
						
					
Reusable Bindings with StepArgumentTransformation
						
[StepArgumentTransformation("the customer")]
public Customer CreateCustomer(Table table)
{
    return table.CreateInstance<Customer>();
}

[Given(@"the customer")]
public void GivenTheCustomer(Customer customer)
{
    _customer = customer;
}
						
					
						
[StepArgumentTransformation("the address")]
public Address CreateAddress(Table table)
{
    return table.CreateInstance<Address>();
}

[Given(@"the address")]
public void GivenTheAddress(Address address)
{
    _address = address;
}
						
					
						
[Given("a new customer and address")]
public void GivenANewCustomerAndAddress()
{
    Customer customer = new Customer
    {
        Salutation = "Miss",
        FirstName = "Liz",
        LastName = "Lemon"
    };

    Address address = new Address
    {
        Line1 = "30 Rockefeller Plaza",
        City = "New York",
        State = "NY",
        Zipcode = "10112"
    };

    _customerSteps.GivenTheCustomer(customer);
    _addressSteps.GivenTheAddress(address);
}
						
					
Another example
						
							When I remove the 6th product
						
					
						
[When(@"I remove the (.*) product")]
public void RemoveProduct(string position)
{
    // Position comes in as 1st, 2nd, 3rd, etc.
    int index = ParsePosition(position);

    // Snip
}
						
					
						
[StepArgumentTransformation(@"(\d+)(?:st|nd|rd|th)")]
public int GetIndex(int index)
{
    return index - 1;
}

[When(@"I remove the (.*) product")]
public void RemoveProduct(int index)
{
    _products.RemoveAt(index);
}

[When(@"I update the (.*) product to (.*)")]
public void UpdateProduct(int index, string productName)
{
    _products[index] = productName;
}
						
					

Create reusable bindings by:

  • Avoiding inheritance
    • Including the Steps class
  • Leveraging dependency injection
  • Removing table arguments with StepArgumentTransformation

Tables

Vertical versus horizontal tables

Vertical Table

						
| Field   | Value                |
| Line 1  | 30 Rockefeller Plaza |
| City    | New York             |
| State   | NY                   |
| Zipcode | 10112                |
						
					

Horizontal Table

						
| Line 1                | City     | State | Zipcode |
| 30 Rockefeller Plaza  | New York | NY    | 10112   |
| 311 South Wacker Dr   | Chicago  | IL    | 60606   |
						
					
Table values should be atomic
						
Scenario: Build a customer
  Given the customer
    | Name           | Address                                   |
    | Miss Liz Lemon | 30 Rockefeller Plaza; New York; NY; 10112 |
						
					
						
Scenario: Build a customer
  Given the customer
    | Salutation | First Name | Last Name |
    | Miss       | Liz        | Lemon     |
  And the address
    | Line 1                | City     | State | Zipcode |
    | 30 Rockefeller Plaza  | New York | NY    | 10112   |
						
					
Table helpers make working with tables easier
						
[Given(@"the following address")]
public void GivenTheFollowingAddress(Table table)
{
    Address address = new Address();

    address.Line1 = table.Rows[0]["Line 1"];
    address.Line2 = table.Rows[0]["Line 2"];
    address.City = table.Rows[0]["City"];
    address.State = table.Rows[0]["State"];
    address.Zipcode = table.Rows[0]["Zipcode"];
}
						
					
						
[Given(@"the following address")]
public void GivenTheFollowingAddress(Table table)
{
    Address address = table.CreateInstance<Address>();
}
						
					
						
[Then(@"the address is")]
public void ValidateAddress(Table table)
{
    Assert.AreEqual(table.Rows[0]["Line 1"], _address.Line1);
    Assert.AreEqual(table.Rows[0]["Line 2"], _address.Line2);
    Assert.AreEqual(table.Rows[0]["City"], _address.City);
    Assert.AreEqual(table.Rows[0]["State"], _address.State);
    Assert.AreEqual(table.Rows[0]["Zipcode"], _address.Zipcode);
}
						
					
						
[Then(@"the address is")]
public void ValidateAddress(Table table)
{
    table.CompareToInstance(_address);
}
						
					

TechTalk.SpecFlow.Assist

  • CreateInstance
  • FillInstance
  • CreateSet
  • CompareToInstance
  • CompareToSet
Customize field mappings with table aliases
						
public class Address
{
    [TableAliases("Street")]
    public string Line1 { get; set; }

    [TableAliases("Township", "Village", "Municipality")]
    public string City { get; set; }

    [TableAliases("Province")]
    public string State { get; set; }

    [TableAliases("Zip", "Zip\\s*Code", "Postal\\s*Code")]
    public string Zipcode { get; set; }
}
						
					
						
Given the address
  | Street          | Village | Province | Postal Code |
  | 110 Prairie Way | Elkhorn | Manitoba | P5A 0A4     |
						
					
Customize value mappings with IValueRetriever and IValueComparer
						
Given the address
  | Street          | Village | Province | Postal Code | Location |
  | 110 Prairie Way | Elkhorn | Manitoba | P5A 0A4     | (42, 88) |
						
					
						
public class Address
{
    [TableAliases("Street")]
    public string Line1 { get; set; }

    [TableAliases("Township", "Village")]
    public string City { get; set; }

    [TableAliases("Province")]
    public string State { get; set; }

    [TableAliases("Zip", "Zip\\s*Code", "Postal\\s*Code")]
    public string Zipcode { get; set; }

    public GeoLocation Location { get; set; }
}
						
					

IValueRetriever

						
public bool CanRetrieve(KeyValuePair<string, string> keyValuePair,
    Type targetType, Type propertyType)
{
    return propertyType == typeof(GeoLocation);
}

public object Retrieve(KeyValuePair<string, string> keyValuePair,
    Type targetType, Type propertyType)
{
    string coordinates = keyValuePair.Value;
    GeoLocation location;

    if (TryGetLocation(coordinates, out location))
        return location;

    throw new Exception(
        $"Unable to parse the location coordinates {coordinates}.");
}
						
					

IValueComparer

						
public bool CanCompare(object actualValue)
{
    return actualValue is GeoLocation;
}

public bool Compare(string expectedValue, object actualValue)
{
    GeoLocation expectedLocation;

    if (TryGetLocation(expectedValue, out expectedLocation))
        return expectedLocation.Equals(actualValue);

    return false;
}
						
					
						
[BeforeTestRun]
public static void RegisterValueMappings()
{
    var geoLocationValueHandler = new GeoLocationValueHandler();
    Service.Instance.RegisterValueRetriever(geoLocationValueHandler);
    Service.Instance.RegisterValueComparer(geoLocationValueHandler);
}
						
					

Avoid manipulating tables in your bindings with:

  • Atomic values
  • The runtime table helpers
  • Table aliases
  • Custom value mappings

Reports

Step Definition Report

Finds unbound scenarios and unused bindings

A red background indicates code not used in any scenarios

A yellow background indicates scenarios with no automation

Wrapping Up

  • Leverage DI and StepArgumentTransformation for reusable bindings
  • Avoid manipulating tables in code
  • Track dead code and unbound scenarios with the Step Definition Report

Further Reading

Thank you!

Presented by Joe Buschmann