Постановка задачи
У себя на проекте мы использем SpecFlow для написания тестов и часто возникала необходимость генерировать уникальные данные для каждого теста. Ну, например, имя товара. Если имя не будет уникальным, то вероятно ваше приложение не даст возможность добавить новую сущность, а ели и даст, то как потом понять, что сейчас работаем с новой, а не той, с прошлого запуска.
В общем захотелось писать в сценариях какое-то ключевое слово и чтоб потом оно заменялось на лету другим значением. Я называю это - макросы. В таком случае, сценарий вида:
When I add an item 'Phone-_random_'
Then the item 'Phone-_random_' should be present in the list
должен замениться на:
When I add an item 'Phone-903456'
Then the item 'Phone-903456' should be present in the list
Самый простой и быстрый вариант - использовать Transformation. Мы даже жили некоторое время с таким решением, но оно не позволяет конвертировать в string и в object, входящим параметром должен быть любой другой тип. Некоторое время мы жили с MacroString типом, который являлся, просто, оберткой над string. Такой вариант не самый удобный и часто вызывал вопросы “Зачем иметь такой класс?”. Так что решил я написать плагин, готорый бы препроцесил сценарии и заменял макросы на результат выполнения функции.
Регистрация плагина
Есть несколько правил, которым нужно следовать, чтобы SpecFlow подхватывал ваш плагин:
- Имя сборки должно заканчиваться на .SpecFlowPlugin
- Должен быть добавлен атрибут [assembly:RuntimePlugin(typeof(Plugin))]
- Должен быть класс, который реализует интерфейс IRuntimePlugin
- В App.config’e тестового проекта в секции plugins нужно добавить наш плагин
<add name="Macro" type="Runtime" />
using Macro.SpecFlowPlugin;
using Macro.SpecFlowPlugin.SpecFlow;
using TechTalk.SpecFlow.Infrastructure;
using TechTalk.SpecFlow.Plugins;
[assembly: RuntimePlugin(typeof(Plugin))]
namespace Macro.SpecFlowPlugin
{
public class Plugin : IRuntimePlugin
{
public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters)
{
runtimePluginEvents.CustomizeGlobalDependencies += RuntimePluginEvents_CustomizeGlobalDependencies;
runtimePluginEvents.CustomizeTestThreadDependencies += RuntimePluginEvents_CustomizeTestThreadDependencies;
}
private void RuntimePluginEvents_CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEventArgs e)
{
e.ObjectContainer.RegisterTypeAs<MacrosLoader, IMacrosLoader>();
}
private void RuntimePluginEvents_CustomizeTestThreadDependencies(object sender, CustomizeTestThreadDependenciesEventArgs e)
{
e.ObjectContainer.RegisterTypeAs<MacrosTestExecutionEngine, ITestExecutionEngine>();
e.ObjectContainer.RegisterFactoryAs(() =>
{
IMacrosLoader loader = e.ObjectContainer.Resolve<IMacrosLoader>();
IProcessor processor = new Processor();
processor.MacroCollection = loader.LoadAll();
return processor;
});
}
}
}
CustomizeGlobalDependencies - событие, которое возникает единажды, при загрузке сборки. В этом же событии мы регистрируем ITestExecutionEngine, который будет обрабатывать текст сценария и тут же мы подгружаем все сборки с макросами.
CustomizeTestThreadDependencies - событие, которое возникает для каждого потока теста. В этом потоке мы будем хранить все макросы чтобы генерировать их каждый раз и не терять значения.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
</configSections>
<specFlow>
<trace traceSuccessfulSteps="false" />
<unitTestProvider name="NUnit" />
<plugins>
<add name="Macro" type="Runtime" />
</plugins>
</specFlow>
</configuration>
Замена
С заменой все достаточно просто: нужно просто создать класс с методом, который будет заменять все слова, которые подпадают под регулярное выражение, на значение функции. Кроме того, значение должно храниться в классе и использоваться при следующем вызове, чтоб все замены одного выражения были эквивалентны.
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Macro.SpecFlowPlugin
{
internal interface IProcessor
{
string Process(string text);
Dictionary<string, IMacro> MacroCollection { get; set; }
}
internal class Processor : IProcessor
{
public Dictionary<string, IMacro> MacroCollection { get; set; }
private readonly Dictionary<string, string> _values;
public Processor()
{
MacroCollection = new Dictionary<string, IMacro>();
_values = new Dictionary<string, string>();
}
public string Process(string text)
{
foreach (var macro in MacroCollection)
{
var matches = Regex.Matches(text, macro.Key);
foreach (Match match in matches)
{
if (!match.Success) continue;
if (!_values.ContainsKey(match.Value))
{
_values.Add(match.Value, macro.Value.Process(match));
}
text = text.Replace(match.Value, _values[match.Value]);
}
}
return text;
}
}
}
Подгрузка классов
Штука в том, что SpecFlow - это плагин для VisualStudio, для которого я пишу плагин Macro.SpecFlow, для которого можно писть плагины… Так вот последние могут включены к любую сборку, эту сборку нужно указать в конфигурации и Macro.SpecFlow подгрузит все макросы.
internal class MacrosLoader : IMacrosLoader
{
private readonly string _directory = Path.GetDirectoryName(new Uri(typeof(Configuration).Assembly.CodeBase).LocalPath);
public Dictionary<string, IMacro> LoadAll()
{
var list = new Dictionary<string, IMacro>();
foreach (var assebly in Configuration.MacroSpecFlow.Assemblies as IEnumerable<AssemblyElement>)
{
var assembly = Assembly.LoadFile(GetFullPath(assebly.Name));
foreach (var type in assembly.GetTypes())
{
if (type.IsClass
&& typeof(IMacro).IsAssignableFrom(type)
&& type.HasCustomAttribute<PatternAttribute>())
{
var value = type.GetCustomAttribute<PatternAttribute>().Value;
list[value] = Activator.CreateInstance(type) as IMacro;
}
}
}
return list;
}
private string GetFullPath(string name)
{
return Path.Combine(_directory, name);
}
}
Nuget и публикация плагина
Последний шаг - это публикация плагина. Для этого нужно создать nuget пакет и загрузить его на nuget.org
Для начала нужно создать nuspec файл и заполнить все соответствующия поля. У плагина есть зависимость на SpecFlow.CustomPlugin, поэтому нужно добавить dependency секцию и указать id пакета.
Код можно посмотреть на моем профиле в GitHub
Comments