Advanced SpecFlow: Restricting Step Definition and Hook Execution with Scoped Bindings

SpecFlow is a tool that allows the writing of business-readable tests that can then be automated in code. If you’re new to SpecFlow check out my Pluralsight course or the resources page to get up to speed before looking at these more advanced topics.

Step definitions and hooks by default have global scope within the test project.

For example, take the following feature:

Feature: SomeFeature

Scenario: Scenario 1
    Given A
    When B
    Then C

Scenario: Scenario 2
    Given A
    When X
    Then Z

Here the “Given A” step shares a step definition, note in the following code there is only one step that matches “Given A”:

[Binding]
public class SomeFeatureSteps
{
    [Given]
    public void Given_A()
    {
        // etc.
    }       

    [When]
    public void When_B()
    {
        // etc.
    }
    
    [When]
    public void When_X()
    {
        // etc.
    }
    
    [Then]
    public void Then_C()
    {
        // etc.
    }
    
    [Then]
    public void Then_Z()
    {
        // etc.
    }
}

If for some reason we want different automation code to run for each of the “Given A” steps, we could simply reword one of the steps, then we would have two different step definitions.

Note that we can’t simply create another binding class with another “Given A” step definition. If we add the following class:

[Binding]
class SomeBindings
{
    [Given]
    public void Given_A()
    {
        // etc.
    }
}

When we try and execute the tests we get an error: “binding error: Ambiguous step definitions found for step 'Given A': SomeBindings.Given_A(), SomeFeatureSteps.Given_A()”.

If for some reason we can’t change the name of the two “Given A” steps, we can use the advanced SpecFlow feature of Scoped Bindings.

Scoping by Scenario Title

The [Scope] attribute can be used to restrict bindings.

Using the examples above, we can restrict the binding in the SomeBindings class to those scenarios that match a specified title.

To do this we use the [Scope] attribute and set it’s Scenario property to the title of the scenario we want to restrict it to:

[Binding]
class SomeBindings
{
    [Given]
    [Scope(Scenario = "Scenario 2")]
    public void Given_A()
    {
        // etc.
    }
}

When we run tests now, we don’t get an error:

NUnit 1.0.0.0 executing tests is started
Given A
-> done: SomeFeatureSteps.Given_A() (0.0s)
When B
-> done: SomeFeatureSteps.When_B() (0.0s)
Then C
-> done: SomeFeatureSteps.Then_C() (0.0s)
Given A
-> done: SomeBindings.Given_A() (0.0s)
When X
-> done: SomeFeatureSteps.When_X() (0.0s)
Then Z
-> done: SomeFeatureSteps.Then_Z() (0.0s)
NUnit 1.0.0.0 executing tests is finished

Notice that the first “Given A” binds to “SomeFeatureSteps.Given_A()”, while the second “Given A” binds to “SomeBindings.Given_A()”.

Scoping by Tag

We can also set scope based on tags.

If we add a tag “sometag” to Scenario 2:

Feature: SomeFeature

Scenario: Scenario 1
    Given A
    When B
    Then C

@sometag
Scenario: Scenario 2
    Given A
    When X
    Then Z

We can modify the scope attribute to restrict binding based on if this tag is applied:

[Binding]
class SomeBindings
{
    [Given]
    [Scope(Tag = "sometag")]
    public void Given_A()
    {
        // etc.
    }
}

(We can also scope by feature name: [Scope(Feature = "SomeFeature")]

Combining Scoping Rules

If multiple properties are set in the same [Binding] attribute, these work in a logical AND fashion.

[Scope(Tag = "sometag", Scenario = "Scenario 1")]

This scope will only match when “sometag” is present AND the scenario is called “Scenario 1”. In the example so far, this binding will never be used as the tag is only applied to “Scenario 2”. In this case, both scenarios will run the same “Given A” – the one without any scoping:

NUnit 1.0.0.0 executing tests is started
Given A
-> done: SomeFeatureSteps.Given_A() (0.0s)
When B
-> done: SomeFeatureSteps.When_B() (0.0s)
Then C
-> done: SomeFeatureSteps.Then_C() (0.0s)
Given A
-> done: SomeFeatureSteps.Given_A() (0.0s)
When X
-> done: SomeFeatureSteps.When_X() (0.0s)
Then Z
-> done: SomeFeatureSteps.Then_Z() (0.0s)
NUnit 1.0.0.0 executing tests is finished

If multiple [Scope] attributes are applied, a logical OR is applied:

[Scope(Tag = "sometag")]
[Scope(Scenario = "Scenario 1")]

When we run the tests now:

NUnit 1.0.0.0 executing tests is started
Given A
-> done: SomeBindings.Given_A() (0.0s)
When B
-> done: SomeFeatureSteps.When_B() (0.0s)
Then C
-> done: SomeFeatureSteps.Then_C() (0.0s)
Given A
-> done: SomeBindings.Given_A() (0.0s)
When X
-> done: SomeFeatureSteps.When_X() (0.0s)
Then Z
-> done: SomeFeatureSteps.Then_Z() (0.0s)
NUnit 1.0.0.0 executing tests is finished

Notice that SomeBindings.Given_A() is used for both scenarios. This is because the scoping will apply if the name is “Scenario 1” OR the tag “sometag” exists. Also notice that the step definition without any scoping “SomeFeatureSteps.Given_A()” doesn’t get used. If 2 steps match, but only one has scoped bindings, SpecFlow will choose the one with scoping applied over the one with no specific scoping applied.

Advanced Scoping

For example, if we want to only execute code if “sometag” AND “someothertag” is applied, we can’t write:

[Scope(Tag = "sometag someotherag")]

As the Tag property only represents a single tag.

We could however just return to having a single “Given A” step where we could make use of the ScenarioContext class:

[Given]
public void Given_A()
{
    if (ScenarioContext.Current.ScenarioInfo.Tags.Contains("sometag") &&
        ScenarioContext.Current.ScenarioInfo.Tags.Contains("someothertag"))
    {
        Console.WriteLine("** Code for both tags");
    }
    else
    {
        Console.WriteLine("** Other code");
    }
}

Now if we add tags to the scenarios:

Feature: SomeFeature

@sometag 
Scenario: Scenario 1
    Given A
    When B
    Then C

@sometag @someothertag
Scenario: Scenario 2
    Given A
    When X
    Then Z

When we run:

NUnit 1.0.0.0 executing tests is started
Given A
** Other code
-> done: SomeFeatureSteps.Given_A() (0.0s)
When B
-> done: SomeFeatureSteps.When_B() (0.0s)
Then C
-> done: SomeFeatureSteps.Then_C() (0.0s)
Given A
** Code for both tags
-> done: SomeFeatureSteps.Given_A() (0.0s)
When X
-> done: SomeFeatureSteps.When_X() (0.0s)
Then Z
-> done: SomeFeatureSteps.Then_Z() (0.0s)
NUnit 1.0.0.0 executing tests is finished

Tag Scoping with Hooks

Hooks (discussed in my previous article) can also be scoped to tags, for example:

[BeforeScenario("someothertag")]

Step Definition Feature Coupling Warning

As the Cucumber Wiki points out: “Feature-coupled step definitions are step definitions that can’t be used across features or scenarios. This is evil because it may lead to an explosion of step definitions, code duplication and high maintenance costs.”

 

To learn more about Scoped Bindings, check out my SpecFlow Tips and Tricks Pluralsight course.

Add comment

Loading