Всем привет!
На Хабре есть отличные статьи по SpecFlow. Я хочу углубиться в данную тему и рассказать про параллельное выполнение тестов, передачу данных между шагами, assist helpers, transformations, hooks и про использование Json в качестве источника данных.
В ��окументации для передачи данных между шагами мы находим следующий пример:
В данном коде используются строковые ключи. Запоминать их и набивать довольно утомительно.
Данную проблему можно решить созданием статического класса с нужными свойствами:
Передача данных теперь выглядит так:
К сожалению, мы не сможем использовать ScenarioContext.Current при запуске тестов в параллель, пока не сделаем нужный Injection
Таким образом мы решили пару проблем: избавились от строковых ключей и обеспечили возможность запуска тестов в параллель. Для желающих поэксперементировать я создал небольшой проект.
Рассмотрим следующий шаг
Довольно часто данные из таблицы вычитывают так
С помощью Assist Helpers чтение тестовых данных выглядит значительно элегантнее. Нам нужно сделать модель с соответствующими свойствами:
и использовать её в CreateInstance
С помощью Transformations, описание тестового шага можно упростить еще больше.
Определяем transformation:
Теперь мы можем использовать нужный тип как параметр в шаге:
Для желающих поэксперементировать — тот же самый проект.
Для простых тестовых данных таблиц SpecFlow более чем достаточно. Однако, бывают тестовые сценарии с большим числом параметров и\или сложной структурой данных. Чтобы использовать подобные тестовые данные, сохраняя читаемость сценария нам потребуются Hooks. Мы воспользуемся хуком [BeforeScenario], чтобы вычитать нужные данные из Json файла. Для этого определим специальные тэги на уровне сценария
и добавим логику обработки в Hooks:
Данный код вычитает содержимое json файла (путь к файлу относительный) в строковую переменную и сохранит ее в данные сценария (ScenarioData.JsonDataSource). Таким образом, мы сможем использовать эти данные, там где требуется
Так как данных в Json может быть много — через тэги можно реализовать и обновление тестовых данных. Желающие могут посмотреть пример в том же проекте.
1. Cucumber
2. Gherkin
3. SpecFlow documentation
4. SpecFlow Wiki
5. Исполняемая спецификация: SpecFlow от А до Я
6. Data Driven Tests & SpecFlow
На Хабре есть отличные статьи по 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
