Всем привет!

На Хабре есть отличные статьи по SpecFlow. Я хочу углубиться в данную тему и рассказать про параллельное выполнение тестов, передачу данных между шагами, assist helpers, transformations, hooks и про использование Json в качестве источника данных.

Параллельное выполнение и передача данных между шагами


В документации для передачи данных между шагами мы находим следующий пример:

// 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;

В данном коде используются строковые ключи. Запоминать их и набивать довольно утомительно.

Данную проблему можно решить созданием статического класса с нужными свойствами:

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

Передача данных теперь выглядит так:

// Loading values into ScenarioContext
ScenarioData.Complex = new ComplexObject();

// Retrieving values from ScenarioContext
var complexObject = ScenarioData.Complex;

К сожалению, мы не сможем использовать ScenarioContext.Current при запуске тестов в параллель, пока не сделаем нужный 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<ComplexObject>(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();
    }
}

Таким образом мы решили пару проблем: избавились от строковых ключей и обеспечили возможность запуска тестов в параллель. Для желающих поэксперементировать я создал небольшой проект.

Assist Helpers and Transformations


Рассмотрим следующий шаг

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

Довольно часто данные из таблицы вычитывают так

[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"];
    ...    
}

С помощью Assist Helpers чтение тестовых данных выглядит значительно элегантнее. Нам нужно сделать модель с соответствующими свойствами:

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

и использовать её в CreateInstance

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

С помощью Transformations, описание тестового шага можно упростить еще больше.

Определяем transformation:

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

Теперь мы можем использовать нужный тип как параметр в шаге:

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

Для желающих поэксперементировать — тот же самый проект.

Hooks и использование Json в качестве источника тестовых данных


Для простых тестовых данных таблиц SpecFlow более чем достаточно. Однако, бывают тестовые сценарии с большим числом параметров и\или сложной структурой данных. Чтобы использовать подобные тестовые данные, сохраняя читаемость сценария нам потребуются Hooks. Мы воспользуемся хуком [BeforeScenario], чтобы вычитать нужные данные из Json файла. Для этого определим специальные тэги на уровне сценария

@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

и добавим логику обработки в 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;
}

Данный код вычитает содержимое json файла (путь к файлу относительный) в строковую переменную и сохранит ее в данные сценария (ScenarioData.JsonDataSource). Таким образом, мы сможем использовать эти данные, там где требуется

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

Так как данных в Json может быть много — через тэги можно реализовать и обновление тестовых данных. Желающие могут посмотреть пример в том же проекте.

Ссылки


1. Cucumber
2. Gherkin
3. SpecFlow documentation
4. SpecFlow Wiki
5. Исполняемая спецификация: SpecFlow от А до Я
6. Data Driven Tests & SpecFlow

Комментарии (0)