Introducción y puesta en marcha
Servicios para todos
Servicios para unos pocos
¿Y el resto?
Sí, es cierto que no todo son servicios en pruebas unitarias de hecho elementos comunes que se suelen encontrar aparte de servicios -que generalmente es lo más complicado de reemplazar- sería la propia verificación del resultado para lo cual existen interesantes librerías como FluentValidation aunque adolece de alguna carencia sobre todo en lo relacionado con recuperación de información de otras fuentes, aunque es verdad que este no es es su principal cometido.
Este escenario es muy habitual y tiene múltiples soluciones, por ejemplo:
private DbSet<T> Set<T>() where T : class
{
var context = this.factory.Services.GetRequiredService<ReplicaDbContext>();
return context.Set<T>();
}
Este es un ejemplo muy sencillo de crear un Set<T> de una base de datos para realizar consultas sobre la misma, también igualmente se puede usar para borrar datos enteros o volverlos a generar para pruebas concretas, existen librerías más especializadas como Respawn que cubren muy bien estos escenarios y con unos pocos métodos de extensión cubren la mayoría de los escenarios.
Otro caso distinto son las entidades que se usan para generar la prueba, principalmente los objetos que tienen que estar disponibles para la prueba, como entidades en base de datos o la propia petición que se envía a una API, en este caso volvemos al problema que comentamos antes, el equilibrio entre no tener pruebas demasiado largas y tampoco generar demasiado código reutilizable.
Estaría bien tener una forma cómoda y rápida de generar objetos sin tener que crear muchos ficheros y evitar generar excesivo código, o incluso código que genera los objetos necesarios para hacer las pruebas, -sí, esto existe-.
Podríamos intentar algo como:
var person = FluentBuilder
.Create<Killer>()
.With(n => n.Name).Set("Hola")
.With(n => n.Surname).Set("Caracola")
.Build();
No deja de ser una forma rápida de asignar valores a propiedades, con la comodidad de poder añadir algo de código de ser necesario, esto es especialmente útil para evitar crear objetos directamente y también en la propia prueba unitaria se puede ver que tipo de objeto se está creando con que propiedades en lugar de una referencia a un objeto común. De igual forma evita generar enormes clases fluent para poder generar cualquier objeto en cualquier escenario posible.
Este sería un ejemplo de implementación:
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace A.Builders
{
public static class FluentBuilder
{
public static FluentBuilderFeatures<TEntity> Create<TEntity>()
where TEntity : class, new()
{
TEntity entity = new TEntity();
return new FluentBuilderFeatures<TEntity>(entity);
}
public static FluentBuilderFeatures<TEntity> Create<TEntity>(
params object[] parameters) where TEntity : class
{
var constructor = typeof(TEntity)
.GetConstructors()
.Where(c => c.GetParameters().Length == parameters.Length
&&
Enumerable.SequenceEqual(
parameters.Select(p => p.GetType()),
c.GetParameters().Select(c => c.ParameterType))
)
.Single();
var entity = (TEntity)constructor.Invoke(parameters);
return new FluentBuilderFeatures<TEntity>(entity);
}
}
public class FluentBuilderFeatures<TEntity>
where TEntity : class
{
private TEntity entity = default(TEntity);
private FluentBuilderFeatures() { }
internal FluentBuilderFeatures(TEntity entity)
{
this.entity = entity;
}
public FluentBuilderProperty<TEntity, TProperty> With<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression)
{
return new FluentBuilderProperty<TEntity, TProperty>(
this, entity, propertyExpression);
}
public TEntity Build()
{
return entity;
}
}
public class FluentBuilderProperty<TEntity, TProperty>
where TEntity : class
{
private TEntity entity = default(TEntity);
private FluentBuilderFeatures<TEntity> builder = default(FluentBuilderFeatures<TEntity>);
private Expression<Func<TEntity, TProperty>> propertyExpression = default(Expression<Func<TEntity, TProperty>>);
private FluentBuilderProperty() { }
internal FluentBuilderProperty(
FluentBuilderFeatures<TEntity> builder,
TEntity entity,
Expression<Func<TEntity, TProperty>> propertyExpression)
{
this.entity = entity;
this.builder = builder;
this.propertyExpression = propertyExpression;
}
public FluentBuilderFeatures<TEntity> Set<TValue>(TValue value)
{
var property = GetPropertyInfo();
property.SetValue(entity, value, null);
return builder;
}
public FluentBuilderFeatures<TEntity> Get<TValue>(ref TValue value)
{
var property = GetPropertyInfo();
value = (TValue)property.GetValue(entity);
return builder;
}
private PropertyInfo GetPropertyInfo()
{
var memberSelectorExpression =
propertyExpression.Body as MemberExpression;
if (memberSelectorExpression != null)
{
var property = memberSelectorExpression.Member as PropertyInfo;
if (property != null)
{
return property;
}
}
throw new InvalidOperationException($"Action on a non-property {memberSelectorExpression.Member.ToString()}");
}
}
}
La idea en general de estos capítulos es poder tener pruebas unitarias lo más auto contenidas posible y que no implique la generación de muchos ficheros, que sean fáciles de mantener y también que sean sencillo crear nuevas para alguien que no esté acostumbrado a crearlas o que acabe de entrar al proyecto.
Es verdad que los frameworks actuales de pruebas ayudan a simplificar la generación de tests y son válidos en muchas ocasiones, pero también es verdad que poder usar el mismo estilo de código en la prueba que en el código de la aplicación ayuda a crear pruebas más rápido y de forma más intuitiva sin tener que consultar en la documentación formas de poder hacer una prueba unitaria por un caso concreto.
Posiblemente estos problemas se mitiguen con la práctica pero también alejan un poco al desarrollador del código original, poder crear una prueba como si fuera código de la aplicación con las mismas librerías y estilo hace una transición más fácil del código de la aplicación al código de las pruebas y ayuda a crear pruebas de de forma fácil y rápida en el que se vea mejor la intención de la prueba sin estar rodeada de código de librerías o de infraestructura que complican ver realmente que casos cubre esa prueba.
Bibliografía
En este caso ChatGPT me ha ayudado para el código Emit, aunque no ha sido capaz de sacarme todos los casos -habrá que probar la versión 4 cuando sea gratis-.
Lyrics
Face The Pain – Stemm
Faster – Within Temptation
Soul Creator – Cinderella
