Всем привет!
На Хабре есть отличные статьи по 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