One nice aspect of SpecFlow is the ability to scope bindings by feature title, scenario title, or tag. Normally bindings are global to the project, but a binding’s scope can be restricted using the Scope
attribute. I like to think of it as similar to the private
and public
class modifiers in C#.
Consider the Gherkin below.
@featureTag | |
Feature: Scoping Example | |
@scenarioTag | |
Scenario: Invoke Service | |
Given the following request body | |
| Content Type | Body | | |
| application/json | { message: "Hello World!" } | | |
When I invoke the service | |
Then the response should have a status code of OK |
It is a single feature with one scenario and two tags. One tag is at the feature level and the other at the scenario level.
The feature’s corresponding steps can be scoped by:
Scenario Title
// Scoped by scenario title | |
[Scope(Scenario = "Invoke Service")] | |
[Binding] | |
public class InvokeServiceSteps | |
{ | |
// Snip | |
} |
Scenario Tag
// Scoped by scenario tag | |
[Scope(Tag = "scenarioTag")] | |
[Binding] | |
public class InvokeServiceSteps | |
{ | |
// Snip | |
} |
Feature Title
// Scoped by feature title | |
[Scope(Feature = "Scoping Example")] | |
[Binding] | |
public class InvokeServiceSteps | |
{ | |
// Snip | |
} |
Feature Tag
// Scoped by feature tag | |
[Scope(Tag = "featureTag")] | |
[Binding] | |
public class InvokeServiceSteps | |
{ | |
// Snip | |
} |
In these snippets, the Scope
attribute is applied at the class level, but you can also put them on individual methods. When the SpecFlow test runner executes the bindings, it will match the Gherkin to methods based on scoping rules. If there are multiple matches, the most restrictive match is used.
Same Gherkin, Different Bindings
To illustrate the usefulness of scoped bindings, consider the following feature that invokes a service with content serialized to XML, JSON, and Form-UrlEncoding. The text of the When
step is the same in the Gherkin for all three scenarios, but each bound method is different. Without scoped bindings, this feature won’t run because the SpecFlow runtime can’t determine which method to execute.
Scenario: Invoke service with XML payload | |
Given a fully populated request object | |
When I invoke the service | |
Then a valid response should be returned | |
Scenario: Invoke service with JSON payload | |
Given a fully populated request object | |
When I invoke the service | |
Then a valid response should be returned | |
Scenario: Invoke service with form-urlencoded payload | |
Given a fully populated request object | |
When I invoke the service | |
Then a valid response should be returned |
[Binding] | |
public class ContentTypeSteps | |
{ | |
[Given(@"a fully populated request object")] | |
public void GivenAFullyPopulatedRequestObject() | |
{ | |
// Snip | |
} | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithXML() | |
{ | |
// Snip | |
} | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithJSON() | |
{ | |
// Snip | |
} | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithFormUrlEncoded() | |
{ | |
// Snip | |
} | |
[Then(@"a valid response should be returned")] | |
public void ThenAValidResponseShouldBeReturned() | |
{ | |
// Snip | |
} | |
} |
Scoped bindings can help. You could scope each method by scenario title.
[Binding] | |
public class ContentTypeSteps | |
{ | |
[Given(@"a fully populated request object")] | |
public void GivenAFullyPopulatedRequestObject() | |
{ | |
// Snip | |
} | |
[Scope(Scenario = "Invoke service with XML payload")] | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithXML() | |
{ | |
// Snip | |
} | |
[Scope(Scenario = "Invoke service with JSON payload")] | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithJSON() | |
{ | |
// Snip | |
} | |
[Scope(Scenario = "Invoke service with form-urlencoded payload")] | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithFormUrlEncoded() | |
{ | |
// Snip | |
} | |
[Then(@"a valid response should be returned")] | |
public void ThenAValidResponseShouldBeReturned() | |
{ | |
// Snip | |
} | |
} |
Or use tags.
@xml | |
Scenario: Invoke service with XML payload | |
Given a fully populated request object | |
When I invoke the service | |
Then a valid response should be returned | |
@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 | |
@formurlencoded | |
Scenario: Invoke service with form-urlencoded payload | |
Given a fully populated request object | |
When I invoke the service | |
Then a valid response should be returned |
[Binding] | |
public class ContentTypeSteps | |
{ | |
[Given(@"a fully populated request object")] | |
public void GivenAFullyPopulatedRequestObject() | |
{ | |
// Snip | |
} | |
[Scope(Tag = "xml")] | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithXML() | |
{ | |
// Snip | |
} | |
[Scope(Tag = "json")] | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithJSON() | |
{ | |
// Snip | |
} | |
[Scope(Tag = "formurlencoded")] | |
[When(@"I invoke the service")] | |
public void WhenIInvokeTheServiceWithFormUrlEncoded() | |
{ | |
// Snip | |
} | |
[Then(@"a valid response should be returned")] | |
public void ThenAValidResponseShouldBeReturned() | |
{ | |
// Snip | |
} | |
} |
While this example works well for demonstrating the power of scoped bindings, I generally avoid coupling step definitions to Gherkin in all but the simplest features. In fact, this type of coupling has been identified as an anti-pattern.
So how would I fix this? That’s the subject of my next post. In the meantime, you can read more about scoped bindings in the SpecFlow documentation.