0. Лирика
Поговорим про unit тестирование. Для больших и возрастных проектов весьма актуальна проблема «толстых» сервисов. Я сейчас говорю про большое количество зависимостей передаваемых в конструктор. Если к этому добавить несколько десятков методов, которые необходимо тестировать, становится очевидно, что тратится много времени на мокирования ненужных частей. Решить проблему поможет автоматизация,. т.е. создание экземпляра необходимого типа и мокирование неиспользованных зависимостей в процессе выполнения.
Получается нам нужно
var myService = new MyService(A.Fake<ISevice1>(), new Sevice2(),
A.Fake<ISevice3>(), A.Fake<ISevice4>(),
A.Fake<ISevice5>(), A.Fake<ISevice6>())
заменить на нечто похожее. Напоминает паттерн builder, не так ли?
var myService = GetInstance<MyService>().With(new Sevice2()).Subject;
Главное не переборщить с автоматизацией. Производительность тоже важна, особенно если в проекте несколько десятков тысяч тестов, которые будут запускаться как локально, так и в настроенном CI.
Разумеется нам не обойтись без рефлексии.
1. Получаем всю необходимую информацию о типе
Для начала уточним, что моки, которые мы будем использовать при создании экземпляра, будут храниться в _overriddenTypes поле, используя следующую логику.
public ObjectBuilder<T> With<TParam>(TParam param)
{
_overriddenTypes.Add(typeof(TParam), param);
return this;
}
Дальше рассмотрим код, который подготавливает информацию для creator. Тут нам как раз-таки пригодится рефлексия.
private T Build()
{
var type = typeof(T);
var constructors = type.GetConstructors().Where(x => x.IsPublic).ToList();
var parameterizedConstructors = constructors.Where(x => x.GetParameters().Any()).ToList();
if (!parameterizedConstructors.Any())
{
// тут можно выбросить исключение
}
var constructor = parameterizedConstructors.Single();
var parametersType = constructor.GetParameters().Select(x => x.ParameterType).ToList();
var arguments = parametersType.Select(x => _overriddenTypes.ContainsKey(x) ? _overriddenTypes[x] : Create.Fake(x)).ToArray();
return GetObject(constructor, parametersType, arguments);
}
В выше представленном коде видно, что для мокирования я использовал FakeItEasy библиотеку.
2. Система кеширования
private T GetObject(ConstructorInfo constructor, List<Type> constructorParametersType, object[] arguments)
{
if (_objectCreatorCache != null)
{
return _objectCreatorCache(arguments);
}
var creator = GetObjectCreator(constructor, constructorParametersType);
_objectCreatorCache = creator;
return creator(arguments);
}
Здесь может быть не совсем понятно, как это работает, и по какой причине для кеширования я использую поле в этом же классе. Будем учитывать, что определение класса выглядит следующим образом:
public class ObjectBuilder<T>
{...}
А так же для кеширования используется статическое поле:
private static Func<object[], T> _objectCreatorCache;
Напомню что статические поля в дженерик классах обладают одной особенностью (на первый взгляд не очевидной): для каждого нового дженерик объекта закрытого уникальным типом будет существовать свой статический член.
3. Создание экземпляра в рантайме
Первый приходящий на ум вариант (на самом деле какое-то время он был единственным) — это использование класса Activator и его метода CreateInstance().
Activator.CreateInstance<T>();
С точки зрения производительности, это не совсем подходящее решение, да и с кеширование тут могут возникнуть проблемы. Получается этот вариант нам не подходит, т.к., если предположить, что в каждом новом тесте нам будет требоваться мокирование зависимойстей сервиса отличных от тех, что использовались в предыдущем тесте.
После внедрения expression's в платформу, появился ещё один, возможно более объёмный способ создания экземпляров типов в рантайме. Его мы и применим.
3.1 Expression object creator
Чем хорош этот подход? В конечном итоге мы получаем скомпилированную лямбду, а не экземпляр объекта. Это позволит использовать кеширование. Я не рекомендую применять данный подход есть требуется единовременное получение экземпляра объекта, Activator справиться с этой задачей значительно быстрее.
private Func<object[], T> GetObjectCreator(ConstructorInfo constructor, List<Type> constructorParametersType)
{
var param = Expression.Parameter(typeof(object[]), "parameters");
var argsExpressions = new Expression[constructorParametersType.Count];
for (var index = 0; index < constructorParametersType.Count; index++)
{
var constantIndex = Expression.Constant(index);
var paramAccessorExp = Expression.ArrayIndex(param, constantIndex);
var paramCastExp = Expression.Convert(paramAccessorExp, constructorParametersType[index]);
argsExpressions[index] = paramCastExp;
}
var newExpression = Expression.New(constructor, argsExpressions);
var lambda = Expression.Lambda(typeof(Func<object[], T>), newExpression, param);
return (Func<object[], T>)lambda.Compile();
}
P.S. Если будет интересно, я могу провести сравнение производительности, а также в деталях описать работу с expression's.
P.S.S. Ниже представлен полный код данного builder'a
public class ObjectBuilder<T>
{
private static Func<object[], T> _objectCreatorCache;
private readonly Dictionary<Type, object> _overriddenTypes;
private readonly Lazy<T> _subject;
public ObjectBuilder()
{
_overriddenTypes = new Dictionary<Type, object>();
_subject = new Lazy<T>(Build);
}
public T Subject => _subject.Value;
public ObjectBuilder<T> With<TParam>(TParam param)
{
if (_subject.IsValueCreated)
{
throw new Exception("Can't change builder options after first call to Object. Please create new one");
}
_overriddenTypes.Add(typeof(TParam), param);
return this;
}
private T Build()
{
var type = typeof(T);
var constructors = type.GetConstructors().Where(x => x.IsPublic).ToList();
var parameterizedConstructors = constructors.Where(x => x.GetParameters().Any()).ToList();
if (!parameterizedConstructors.Any())
{
// тут пожалуй можно выбросить исключение
}
var constructor = parameterizedConstructors.Single();
var constructorParametersType = constructor.GetParameters().Select(x => x.ParameterType).ToList();
var arguments = constructorParametersType.Select(x => _overriddenTypes.ContainsKey(x) ? _overriddenTypes[x] : Create.Fake(x)).ToArray();
return GetObject(constructor, constructorParametersType, arguments);
}
private T GetObject(ConstructorInfo constructor, List<Type> constructorParametersType, object[] arguments)
{
if (_objectCreatorCache != null)
{
return _objectCreatorCache(arguments);
}
var creator = GetObjectCreator(constructor, constructorParametersType);
_objectCreatorCache = creator;
return creator(arguments);
}
private Func<object[], T> GetObjectCreator(ConstructorInfo constructor, List<Type> constructorParametersType)
{
var param = Expression.Parameter(typeof(object[]), "parameters");
var argsExpressions = new Expression[constructorParametersType.Count];
for (var index = 0; index < constructorParametersType.Count; index++)
{
var constantIndex = Expression.Constant(index);
var paramAccessorExp = Expression.ArrayIndex(param, constantIndex);
var paramCastExp = Expression.Convert(paramAccessorExp, constructorParametersType[index]);
argsExpressions[index] = paramCastExp;
}
var newExpression = Expression.New(constructor, argsExpressions);
var lambda = Expression.Lambda(typeof(Func<object[], T>), newExpression, param);
return (Func<object[], T>)lambda.Compile();
}
}
Комментарии (5)
ilya-chumakov
07.03.2018 19:46+1Для создания иерархии объектов и разрешения зависимостей можно было просто использовать DI-контейнер. Он для этого и предназначен.
Interreto
09.03.2018 14:45-1Да, причем тот же что и в проекте, и это было-бы более органично, поскольку паралельно тестировалась интеграция контейнера с юнитами.
dimedroll2211 Автор
09.03.2018 14:54+2Проблема использования DI это скорость работы. Все работает достаточно нерасторопно. Связанно это с пересозданием контейнера каждый раз при изменении мокируемых сервисов. А к главным чертами юнит тестирования можно отнести быстроту выполнения, краткость, читаемость и тестирование одного изолированного юнита. Так же хочется напомнить что для тестирования интеграции применяется интеграционное тестирование.
Veikedo
AutoMoq AutoFixture
А вообще, мне кажется, проблемы у вас поглубже
savelievser
Жажда писать свои велосипеды, чтобы заюзать любимую фичу, видимо не победима)