📜 ⬆️ ⬇️

Do you know where you can apply expression's in your project or optimize the creation of tests

0. Lyrics


Let's talk about unit testing. For large and age-related projects, the problem of “fat” services is highly relevant. I'm talking about a large number of dependencies passed to the constructor. If we add to this several dozens of methods that need to be tested, it becomes obvious that a lot of time is being spent on mocking unnecessary parts. Automation will help solve the problem. those. creating an instance of the required type and mocking unused dependencies during execution.

It turns out we need

var myService = new MyService(A.Fake<ISevice1>(), new Sevice2(), A.Fake<ISevice3>(), A.Fake<ISevice4>(), A.Fake<ISevice5>(), A.Fake<ISevice6>()) 

replace with something similar. Reminds pattern builder, is not it?
')
  var myService = GetInstance<MyService>().With(new Sevice2()).Subject; 

The main thing is not to overdo it with automation. Performance is also important, especially if the project has several tens of thousands of tests that will be run both locally and in a configured CI.

Of course we can not do without reflection.

1. Get all the necessary information about the type


To begin with, we clarify that the mocks that we will use when creating an instance will be stored in the _overriddenTypes field using the following logic.

  public ObjectBuilder<T> With<TParam>(TParam param) { _overriddenTypes.Add(typeof(TParam), param); return this; } 

Next, consider the code that prepares the information for the creator. Here we just need a reflection.

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

In the above code, you can see that I used the FakeItEasy library for mocking.

2. Caching system


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

Here it may not be entirely clear how this works, and for what reason I use a field in the same class for caching. We take into account that the definition of a class is as follows:

 public class ObjectBuilder<T> {...} 

A static field is also used for caching:

 private static Func<object[], T> _objectCreatorCache; 

Let me remind you that static fields in generic classes have one feature (not obvious at first glance): for each new generic object closed by a unique type there will be a static member.

3. Creating an instance in runtime


The first option that comes to mind (in fact, for some time it was the only one) is the use of the Activator class and its CreateInstance () method.

 Activator.CreateInstance<T>(); 

From the point of view of performance, this is not an appropriate solution, and problems with caching may arise here. It turns out this option does not suit us, because if we assume that in each new test we will need a mocking of service dependencies different from those used in the previous test.

After introducing expression's into the platform, one more, perhaps more voluminous way of creating type instances in runtime appeared. We will apply it.

3.1 Expression object creator


What is this approach good for? Ultimately, we get a compiled lambda, not an object instance. This will allow the use of caching. I do not recommend using this approach; one-time receipt of an object instance is required; Activator is able to cope with this task much faster.

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

PS If it will be interesting, I can make a performance comparison, as well as describe in detail the work with expression's.

PSS Below is the full code of this builder

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

Source: https://habr.com/ru/post/350688/


All Articles