Steps are the building blocks of SpecFlow. Each Given/When/Then line in a SpecFlow scenario represents a step, and steps should be reused across features and scenarios to test your application from different angles. When you’re building a low-level scenario, you may want to use very specific steps. In a higher-level feature, you may want to perform the same tasks but in a less granular fashion. Wouldn’t it be nice if you could create a “super-step” that just calls the necessary sub-steps?
Well, guess what? You can, and it’s really easy to do. First, let’s build some fake code to work with. I created a simple PersonRepository that lets me add and save Person objects. Here are the classes and my initial SpecFlow test.
Person.cs
using System; namespace samples.SpecFlowDemo { public class Person { public int Id { get; set; } public string Name { get; set; } public DateTime? DateOfBirth { get; set; } } }
PersonRepository.cs
using System; using System.Collections.Generic; namespace samples.SpecFlowDemo { public static class PersonRepository { private static readonly Dictionary<int, Person> Persons; static PersonRepository() { Persons = new Dictionary<int, Person>(); } public static Person Get(int id) { if (!Persons.ContainsKey(id)) { return null; } return Persons[id]; } public static bool Save(Person person) { if (person == null) { return false; } if (!Persons.ContainsKey(person.Id)) { Persons.Add(person.Id, person); } Persons[person.Id] = person; return true; } } }
Person_Add.feature
Feature: Person_Add In order track person records As a records manager I need to add new persons Scenario: Add a person Given I have a new person record with the following properties | id | name | date of birth | | 100 | Rodney | 2/20/1950 | When I save the person Then the person is saved successfully And I can retrieve the person by ID
Person_AddSteps.cs
using Microsoft.VisualStudio.TestTools.UnitTesting; using TechTalk.SpecFlow; using TechTalk.SpecFlow.Assist; namespace samples.SpecFlowDemo.SpecFlow { [Binding] public class Person_AddSteps { [Given(@"I have a new person record with the following properties")] public void GivenIHaveANewPersonRecordWithTheFollowingProperties(Table table) { ScenarioContext.Current.Set<Person>( table.CreateInstance<Person>()); } [When(@"I save the person")] public void WhenISaveThePerson() { var r = PersonRepository.Save( ScenarioContext.Current.Get<Person>()); ScenarioContext.Current.Set<bool>(r, "SaveResult"); } [Then(@"the person is saved successfully")] public void ThenThePersonIsSavedSuccessfully() { Assert.IsTrue( ScenarioContext.Current.Get<bool>("SaveResult"), "SaveResult"); } [Then(@"I can retrieve the person by ID")] public void ThenICanRetrieveThePersonByID() { var expected = ScenarioContext.Current.Get<Person>(); var actual = PersonRepository.Get(expected.Id); Assert.AreSame(expected, actual); } } }
Now let’s say I want to test the PersonRepository’s ability to update records. In order to update a record, the record needs to exist. I could reuse the add feature’s “I have a person” step with a provided table of properties and its “I save the person” step, but it would be nice if I didn’t have to call both of those steps and provide data each time I needed to do something with an existing record.
I can avoid the repetition by calling those steps from within a new step that I’ll create called “I have an existing person record.” Here’s the code for my update feature.
Person_Update.feature
Feature: Person_Update In order track person records As a records manager I need to update existing persons Scenario: Update a person Given I have an existing person record When I change the person name to "Rocko" And I save the person Then the person is saved successfully And I can retrieve the person by ID And the person name was saved as "Rocko"
Person_UpdateSteps.cs (Note the highlighted lines. The only “gotcha” that I ran into is that the steps class must inherit from SpecFlow’s Steps class in order to access the Given/When/Then functions.)
using Microsoft.VisualStudio.TestTools.UnitTesting; using TechTalk.SpecFlow; namespace samples.SpecFlowDemo.SpecFlow { [Binding] public class Person_UpdateSteps : Steps { [Given(@"I have an existing person record")] public void GivenIHaveAnExistingPersonRecord() { var header = new[] { "Field", "Value" }; var t = new Table(header); t.AddRow("id", "100"); t.AddRow("name", "Fred"); t.AddRow("date of birth", "12/15/1990"); Given("I have a new person record with the following properties", t); When("I save the person"); } [When(@"I change the person name to ""(.*)""")] public void WhenIChangeThePersonNameTo(string p0) { var p = ScenarioContext.Current.Get<Person>(); p.Name = p0; } [Then(@"the person name was saved as ""(.*)""")] public void ThenThePersonNameWasSavedAs(string p0) { var p = ScenarioContext.Current.Get<Person>(); Assert.AreEqual(p0, p.Name); } } }
You can read more about calling steps from step definitions in the SpecFlow documentation.
You can also download the code for this example from GitHub.
One thought on “Calling Steps From Steps in SpecFlow”