Useful SpecFlow Features

  • Tutorial
Hello!

On Habré there are excellent articles on SpecFlow. I want to delve into this topic and talk about running tests in parallel, passing data between steps, assist helpers, transformations, hooks, and about using Json as a data source.

Parallel execution and data transfer between steps


In the documentation for transferring data between steps, we find the following example:

// Loading values into ScenarioContext
ScenarioContext.Current["id"] = "value";
ScenarioContext.Current["another_id"] = new ComplexObject();
// Retrieving values from ScenarioContext
var id = ScenarioContext.Current["id"];
var complexObject = ScenarioContext.Current["another_id"] As ComplexObject;

This code uses string keys. Memorizing and typing them is quite tedious.

This problem can be solved by creating a static class with the necessary properties:

public static class ScenarioData
{
    public static ComplexObject Complex
    {
        get => ScenarioContext.Current.Get(nameof(Complex));
        set => ScenarioContext.Current.Set(value, nameof(Complex));
    }
}

Data transfer now looks like this:

// Loading values into ScenarioContext
ScenarioData.Complex = new ComplexObject();
// Retrieving values from ScenarioContext
var complexObject = ScenarioData.Complex;

Unfortunately, we will not be able to use ScenarioContext.Current when running tests in parallel until we make the necessary Injection

// абстрактынй класс для описания шагов
[Binding]
public abstract class ScenarioSteps
{
    protected ScenarioSteps(ScenarioContext scenarioContext, FeatureContext featureContext)
    {
        FeatureContext = featureContext;
        ScenarioContext = scenarioContext;
        ScenarioData = new ScenarioData(scenarioContext);
    }
    public FeatureContext FeatureContext { get; }
    public ScenarioContext ScenarioContext { get; }
    public ScenarioData ScenarioData { get; }
}
// модифицированный ScenarioData
public class ScenarioData
{
    private readonly ScenarioContext _context;
    public ScenarioData(ScenarioContext context)
    {
        _context = context;
    }
    public ComplexObject Complex
    {
        get => _context.Get(nameof(Complex));
        set => _context.Set(value, nameof(Complex));
    }
}
// конкретный класс для описания шагов
[Binding]
public class ActionSteps : ScenarioSteps
{
    public ActionSteps(ScenarioContext scenarioContext, FeatureContext featureContext)
        : base(scenarioContext, featureContext)
    {
    }
    [When(@"user uses complex object")]
    public void WhenUserUsesComplexObject()
    {
        ScenarioData.Complex = new ComplexObject();
    }
}

Thus, we solved a couple of problems: got rid of string keys and provided the ability to run tests in parallel. For those who want to experiment, I created a small project.

Assist Helpers and Transformations


Consider the next step

When user starts rendering
| SourceType   | PageOrientation | PageMediaSizeName |
| set-01-valid | Landscape       | A4                |

Quite often, data from a table is subtracted like this

[When(@"user starts Rendering")]
public async Task WhenUserStartsRendering(Table table)
{
    var sourceType = table.Rows.First()["SourceType"];
    var pageOrientation = table.Rows.First()["PageOrientation"];
    var pageMediaSizeName = table.Rows.First()["PageMediaSizeName"];
    ...    
}

Using Assist Helpers, reading test data looks much more elegant. We need to make a model with the appropriate properties:

public class StartRenderingRequest
{
    public string SourceType { get; set; }
    public string PageMediaSizeName { get; set; }
    public string PageOrientation { get; set; }
}

and use it in CreateInstance

[When(@"user starts Rendering")]
public async Task WhenUserStartsRendering(Table table)
{
    var request = table.CreateInstance();
    ...    
}

With Transformations , the description of the test step can be simplified even further.

Define the transformation:

[Binding]
public class Transforms
{
    [StepArgumentTransformation]
    public StartRenderingRequest StartRenderingRequestTransform(Table table)
    {
       return table.CreateInstance();
    }
}

Now we can use the desired type as a parameter in the step:

[When(@"user starts Rendering")]
public async Task WhenUserStartsRendering(StartRenderingRequest request)
{
    // we have implemented transformation, so we use StartRenderingRequest as a parameter
    ...    
}

For those who want to experiment - the same project.

Hooks and using Json as a source of test data


SpecFlow tables are more than enough for simple test data. However, there are test scenarios with a large number of parameters and / or a complex data structure. In order to use such test data, while maintaining the readability of the script, we need Hooks . We will use the [BeforeScenario] hook to read the necessary data from the Json file. To do this, define special tags at the script level

@jsonDataSource @jsonDataSourcePath:DataSource\FooResponse.json
Scenario: Validate Foo functionality
Given user has access to the Application Service
When user invokes Foo functionality
| FooRequestValue |
| input           |
Then Foo functionality should complete successfully

and add the processing logic to the hooks:

[BeforeScenario("jsonDataSource")]
public void BeforeScenario()
{
    var tags = ScenarioContext.ScenarioInfo.Tags;
    var jsonDataSourcePathTag = tags.Single(i => i.StartsWith(TagJsonDataSourcePath));
    var jsonDataSourceRelativePath = jsonDataSourcePathTag.Split(':')[1];
    var jsonDataSourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, jsonDataSourceRelativePath);
    var jsonRaw = File.ReadAllText(jsonDataSourcePath);
    ScenarioData.JsonDataSource = jsonRaw;
}

This code subtracts the contents of the json file (relative path to the file) into a string variable and saves it to the script data (ScenarioData.JsonDataSource). Thus, we can use this data where required

[Then(@"Foo functionality should complete successfully")]
public void ThenFooFunctionalityShouldCompleteSuccessfully()
{
    var actual = ScenarioData.FooResponse;
    var expected = JsonConvert.DeserializeObject(ScenarioData.JsonDataSource);
    actual.FooResponseValue.Should().Be(expected.FooResponseValue);
}

Since there can be a lot of data in Json, updating the test data can also be implemented through tags. Those interested can see an example in the same project.

References


1. Cucumber
2. Gherkin
3. SpecFlow documentation
4. SpecFlow Wiki
5. Executable specification: SpecFlow A to Z
6. Data Driven Tests & SpecFlow

Also popular now: