Presented by Joe Buschmann
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
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
[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 }
}
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?
TechTalk.SpecFlow.BindingException : Ambiguous step definitions found for step 'Given Invoke Service'
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?
private readonly ScenarioContext _scenarioContext;
public AddressSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
}
[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();
}
}
@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
}
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);
}
[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);
}
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:
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 |
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 |
[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);
}
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 |
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:
Finds unbound scenarios and unused bindings
A red background indicates code not used in any scenarios
A yellow background indicates scenarios with no automation
Further Reading
Presented by Joe Buschmann