Хабрахабр

[Из песочницы] А вы знаете где можно применить expression’s в вашем проекте или оптимизация создания тестов

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(); } }

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть