This is c# based Specflow Step Definitions tutorial will explore intricacies of step definitions. This post follows the beginner tutorial I wrote before. I will recommend that you go through that before reading this post. Step definitions look simple but they have lot of hidden power. So without wasting any more time, let us as usual go for the WHY followed by HOW.
WHY
We humans are greedy. We always want more. In case of Specflow step definition, users were quick to ask
- Can we pass data from specflow step definition to background implementation. (Hint: Yes)
- Can we share data between the implementations of specflow step definitions. (Hint: Yes)
- Can we pass a table of data from specflow step definition to background implementation. (Hint: Yes)
- Can we keep implementations in some other file and make step definitions use them instead. (Hint: Yes (TODO))
- Can we share data between such implementations. (Hint: Yes (TODO))
- What else I can do with specflow step definition? (Check this (TODO))
- Was “Batman v Superman : Dawn of Justice” a good idea. (Definitive answer: NO, Leave Batman alone.)
HOW
So let us start tackling each of these questions one at a time.
Pass data from specflow step definition to background implementation
First things first. I will use the same project I used in the beginner tutorial. This time I will add another file called BatmobileControl.cs
. This will work as system under test. These are the contents
namespace Batman { public class BatMobileControl { public int Duration { get; set; } public string Mode { get; set; } public void SetDriveMode(string mode, int duration) { Mode = mode; Duration = duration; } public bool GetDriveMode(string mode, int duration) { return Mode == mode && Duration == duration; } } }
Let us add a feature file called Batmobile.feature
. If you have gone through the beginner post, then you can understand the contents.
Feature: Batmobile In order to travel on dangerous missions As a Batman I want drive Batmobile @mytag Scenario: Drive batmobile Given I have started Batmobile And I set it to drive on "stealth" mode for 40 mins Then Batmobile drives on "stealth" mode for 40 mins
Here I am passing parameters from the step definition to the background implementation. See the double quotes around word stealth? That means I am telling specflow to interpret it as a string parameter and generate the function signature of the background implementation accordingly. The number 40 will automatically be interpreted by specflow as an integer parameter.
Let us generate the step definitions. I will set style as regular expression in the attributes.
Here is the step file BatmobileSteps.cs
. I have added the implementation of the steps to save time.
using TechTalk.SpecFlow; using Xunit; namespace Batman { [Binding] public class BatmobileSteps { private BatmobileControl _barBatmobileControl; [Given(@"I have started Batmobile")] public void GivenIHaveStartedBatmobile() { _barBatmobileControl = new BatmobileControl(); } [Given(@"I set it to drive on ""(.*)"" mode for (.*) mins")] public void GivenISetItToDriveOnModeForMins(string driveMode, int duration) { _barBatmobileControl.SetDriveMode(driveMode, duration); } [Then(@"Batmobile drives on ""(.*)"" mode for (.*) mins")] public void ThenBatmobileDrivesOnModeForMins(string driveMode, int duration) { Assert.True(_barBatmobileControl.GetDriveMode(driveMode, duration)); } } }
Check out the signature for methods GivenISetItToDriveOnModeForMins and ThenBatmobileDrivesOnModeForMins. Specflow by default names the parameters p0, p1 and so on. I changed them to something more readable. The parameter types are of interest to us. See how specflow correctly assigned them as string and integer type.
Let us run this in debug mode to see if the data does get passed.
And the test passes.
We just passed data to the step definition implementation. If you want even more details then go to Deep Dive post (coming soon). Moving on to the next.
We have already seen one way of sharing data between the Specflow Step Definitions. See the file BatmobileSteps.cs
. We maintain a private instance of BatmobileControl class. We populate it in the GivenIHaveStartedBatmobile() function. The rest of the functions then happily use this instance to do their work. However there is one major issue here. It is very much possible to have Specflow Step Definitions exist in different classes. Then this trick will not work.
Enter ScenarioContext.
As the name tells, this is a way to share data between the steps of a “Given Scenario”. Not between scenarios. Now with that thing cleared let us change the feature file to show the impact this has on the scenario.
Feature: Batmobile In order to travel on dangerous missions As a Batman I want drive Batmobile @mytag Scenario: Drive batmobile Given I have started Batmobile And I set it to drive on "stealth" mode for 40 mins Then Batmobile drives accordingly
Check the last step. We need not pass data to it again. It will use data which was stored by the step preceding it in the implementation file. As shown below in BatmobileSteps.cs
using TechTalk.SpecFlow; using Xunit; namespace Batman { [Binding] public class BatmobileSteps { private BatmobileControl _barBatmobileControl; [Given(@"I have started Batmobile")] public void GivenIHaveStartedBatmobile() { _barBatmobileControl = new BatmobileControl(); } [Given(@"I set it to drive on ""(.*)"" mode for (.*) mins")] public void GivenISetItToDriveOnModeForMins(string driveMode, int duration) { ScenarioContext.Current.Add("Drive mode", driveMode); ScenarioContext.Current.Add("Duration", duration); _barBatmobileControl.SetDriveMode(driveMode, duration); } [Then(@"Batmobile drives accordingly")] public void ThenBatmobileDrivesAccordingly() { Assert.True(_barBatmobileControl.GetDriveMode((string)ScenarioContext.Current["Drive mode"], (int)ScenarioContext.Current["Duration"])); } } }
See how we add the data we want to store into the ScenarioContext in form of key value pair in method GivenISetItToDriveOnModeForMins(). In the method ThenBatmobileDrivesAccordingly() we extract this data. The only downside is the casting which we have to do.
Let us debug to see the data we are getting.
And the test passes.
What if you are not happy with the cast operations needed for ScenarioContext. There is a better way of doing this sharing of data between Specflow Step Definitions.
Enter context injection.
It runs on the dependency injection support which Specflow provides. We have to create an class which will contain the data to be shared between the steps of a given scenario. Then we inject this class into the constructor of the step definition class. Then it is available to all the step definition implementations. “Talk is cheap. Show me code”. Let us refactor the sample we have to check this feature.
I will add first the class which will have the necessary wiring to store the data inside. This data will be later shared between the steps. Here is the class which I named as BatmobileControl.cs
.
using System; using System.Collections.Generic; using TechTalk.SpecFlow; namespace Batman { public class BatmobileControl { public int Duration { set; get; } public string Mode { get; set; } public Boolean OperationFailed { get; set; } private Dictionary_driveModeDictionary = new Dictionary (); public void SetDriveMode(string mode, int duration) { if (_driveModeDictionary.ContainsKey(mode)) { if (_driveModeDictionary[mode] < duration) { Mode = mode; Duration = duration; OperationFailed = false; } } OperationFailed = true; } public bool GetDriveMode(string mode, int duration) { return Mode == mode && Duration == duration; } } }
I will just change the BatmobileSteps.cs
to show this cool feature in action.
using TechTalk.SpecFlow; using Xunit; namespace Batman { [Binding] public class BatmobileSteps { private readonly BatmobileControl _barBatmobileControl; public BatmobileSteps(BatmobileControl batmobileControl) { _barBatmobileControl = batmobileControl; } [Given(@"I have started Batmobile")] public void GivenIHaveStartedBatmobile() { //_barBatmobileControl = new BatmobileControl(); } [Given(@"I set it to drive on ""(.*)"" mode for (.*) mins")] public void GivenISetItToDriveOnModeForMins(string driveMode, int duration) { _barBatmobileControl.Mode = driveMode; _barBatmobileControl.Duration = duration; _barBatmobileControl.SetDriveMode(driveMode, duration); } [Then(@"Batmobile drives accordingly")] public void ThenBatmobileDrivesAccordingly() { var driveMode = _barBatmobileControl.Mode; var duration = _barBatmobileControl.Duration; Assert.True(_barBatmobileControl.GetDriveMode(driveMode, duration)); } } }
Important lines:
- Line 11: We have a constructor for the class containing the step definitions here. Constructor injection in action. Check my post on dependency injection for more details.
- Line 13: Dependency framework in Specflow instantiates an object of BatmanmobileControl type and passes it to constructor. See my post on Unity for more details.
- Line 19: No longer need to new up an instance of BatmanmobileControl type!!
- Line 25, 26: Saving the data passed into the BatmanmobileControl instance.
- Line 33, 34: Accessing the data saved in BatmanmobileControl instance. No casting needed. Woo hoo.
You can check my post on dependency injection and Unity to get some background on dependency injection if needed.
So does it work?
This works even if you split the step definitions between two separate classes. Just make sure that each class has got a constructor which is taking an instance of the class which contains the common data which you want to share between the steps.
One thing to remember is that this object lifespan is limited to the time the scenario is executing. Next scenario will gets a new instance of this object. So you cannot pass data between the scenarios.
Pass a table of data from specflow step definition to background implementation
To pass data from Specflow Step Definitions the feature file Batmobile.feature
changes a little bit. Here it is.
Feature: Batmobile In order to travel on dangerous missions As a Batman I want drive Batmobile Scenario: Drive mode support Given Batmobile can support these modes | Mode | Duration | | Pursuit | 60 | | Battle | 20 | | Stealth | 40 | And I set it to drive on "Battle" mode for 40 mins Then Batmobile does not not change drive mode
As highlighted lines show, the table is specflow world is not pretty. But it gets the job done. When you generate the background implementation you will see that the generated function will be taking a parameter of type Table which is provided by Specflow. Then you can chomp your way through the table to get the data you want. Here is the background implementation file BatmobileSteps.cs
.
using TechTalk.SpecFlow; using Xunit; namespace Batman { [Binding] public class BatmobileSteps { private readonly BatmobileControl _barBatmobileControl; public BatmobileSteps(BatmobileControl batmobileControl) { _barBatmobileControl = batmobileControl; } [Given(@"Batmobile can support these modes")] public void GivenBatmobileCanSupportTheseModes(Table table) { _barBatmobileControl.InitializeDriveModes(table); } [Given(@"I set it to drive on ""(.*)"" mode for (.*) mins")] public void GivenISetItToDriveOnModeForMins(string driveMode, int duration) { _barBatmobileControl.Mode = driveMode; _barBatmobileControl.Duration = duration; _barBatmobileControl.SetDriveMode(driveMode, duration); } [Then(@"Batmobile does not not change drive mode")] public void ThenBatmobileDoesNotNotChangeDriveMode() { Assert.True(_barBatmobileControl.OperationFailed); } } }
As you can see in the highlighted text, the implementation now takes a parameter of type table. We add the function InitializeDriveModes
to BatmobileControl.cs
.
using System; using System.Collections.Generic; using System.Linq; using TechTalk.SpecFlow; namespace Batman { public class BatmobileControl { public int Duration { set; get; } public string Mode { get; set; } public Boolean OperationFailed { get; set; } private Dictionary_driveModeDictionary = new Dictionary (); public void SetDriveMode(string mode, int duration) { if (_driveModeDictionary.ContainsKey(mode)) { if (_driveModeDictionary[mode] < duration) { Mode = mode; Duration = duration; OperationFailed = false; } } OperationFailed = true; } public bool GetDriveMode(string mode, int duration) { return Mode == mode && Duration == duration; } public void InitializeDriveModes(Table driveModeTable) { _driveModeDictionary.Add(driveModeTable.Rows[0][0], int.Parse(driveModeTable.Rows[0][1])); _driveModeDictionary.Add(driveModeTable.Rows[1][0], int.Parse(driveModeTable.Rows[1][1])); _driveModeDictionary.Add(driveModeTable.Rows[2][0], int.Parse(driveModeTable.Rows[2][1])); } } }
You can see in InitializeDriveModes
that we can access the individual rows. The indexing is zero based. You can LINQ your way through this but I will keep things real simple. There are many operations which the Table supports. Do have a look. I changed SetDriveMode
so that now it checks whether the drive mode is supported. If not then it sets the OperationFailed
flag to true.
I have set up the feature file such that it tries to set the Batmobile into a mode for more than supported duration. I expect a failure as seen in the last step of the feature file. In the implementation of that step, I check the OpreationFailed
flag. So does the test pass? Indeed it does.