Aggregate
El AggregateAttribute
es un atributo personalizado diseñado para facilitar la creación de pruebas unitarias que validen el comportamiento de los agregados en tu modelo de dominio. Este atributo permite generar automáticamente instancias de agregados, ya sea utilizando el constructor o un método estático Create
, lo que agiliza el proceso de pruebas y asegura una cobertura exhaustiva.
¿Cómo Funciona?
El AggregateAttribute
funciona como un proveedor de datos para pruebas unitarias utilizando el framework xUnit. Cuando se aplica a un método de prueba, el atributo se encarga de:
- Escanear el ensamblado: Identifica todas las clases que son agregados (clases que heredan de
AggregateRoot
). - Crear instancias: Para cada agregado encontrado, crea una instancia utilizando el constructor o el método estático
Create
(si existe). - Proveer datos: Suministra la instancia del agregado junto con su tipo y los valores de los parámetros utilizados en su creación como datos para la prueba unitaria.
Métodos
GetData
GetData(MethodInfo testMethod)
Este método es el punto central del AggregateAttribute
. Se encarga de realizar las siguientes acciones:
- Localizar Agregados: Busca en el ensamblado especificado por el parámetro genérico
TAssemblyScan
todas las clases que son agregados (heredan deAggregateRoot
). - Crear Instancias: Crea una instancia de cada agregado utilizando el constructor o el método estático
Create
.- Constructor: Si el parámetro
useCreateMethod
esfalse
, se utiliza el constructor que recibe unGuid
como parámetro. - Método
Create
: Si el parámetrouseCreateMethod
estrue
, se busca un método estático llamadoCreate
en la clase del agregado. Si no se encuentra, se lanza una excepción.
- Constructor: Si el parámetro
- Retornar Datos: Retorna una secuencia de arrays de objetos, cada array contiene:
- El
Type
del agregado. - La instancia del agregado.
- Un diccionario con los
ParameterInfo
y los valores utilizados en la creación del agregado.
- El
Implementación
El AggregateAttribute
se implementa como un atributo personalizado que hereda de DataAttribute
de xUnit. Recibe un parámetro genérico TAssemblyScan
que indica el ensamblado en el que buscar los agregados y un parámetro booleano useCreateMethod
para indicar si se debe usar el método estático Create
o el constructor para crear instancias.
namespace CodeDesignPlus.Net.xUnit.Microservice.Attributes;
/// <summary>/// A custom attribute for providing data to test methods that validate aggregates./// </summary>/// <remarks>/// Initializes a new instance of the <see cref="AggregateAttribute{TAssemblyScan}"/> class./// </remarks>/// <typeparam name="TAssemblyScan">The type of the assembly to scan for aggregates.</typeparam>/// <param name="useCreateMethod">Indicates whether to use the static Create method or the constructor to create instances of aggregates.</param>[AttributeUsage(AttributeTargets.Method)]public class AggregateAttribute<TAssemblyScan>(bool useCreateMethod) : DataAttribute where TAssemblyScan: IErrorCodes{ /// <summary> /// Gets the data for the test method. /// </summary> /// <param name="testMethod">The test method for which data is being provided.</param> /// <returns>An enumerable of object arrays representing the data for the test method.</returns> public override IEnumerable<object[]> GetData(MethodInfo testMethod) { var aggregates = typeof(TAssemblyScan).Assembly .GetTypes() .Where(x => !x.FullName.StartsWith("Castle") || !x.FullName.Contains("DynamicProxyGenAssembly")) .Where(x => x.IsClass && !x.IsAbstract && x.IsSubclassOf(typeof(AggregateRoot))) .ToList();
foreach (var aggregate in aggregates) { object instance; Dictionary<ParameterInfo, object> values;
if (useCreateMethod) { var nameConstructor = aggregate.GetMethod("Create", BindingFlags.Static | BindingFlags.Public);
if (nameConstructor is null) throw new CoreException($"The {aggregate.Name} class does not have a static Create method.");
values = nameConstructor.GetParameters().GetParameterValues(); instance = nameConstructor.Invoke(null, [.. values.Values]); } else { var constructor = aggregate.GetConstructor([typeof(Guid)]); values = new Dictionary<ParameterInfo, object> { { constructor.GetParameters()[0], Guid.NewGuid() } }; instance = constructor.Invoke([.. values.Values]); }
yield return new object[] { aggregate, instance, values }; } }}
Ejemplo de Uso
El siguiente ejemplo muestra cómo utilizar el AggregateAttribute
en un método de prueba unitaria:
namespace CodeDesignPlus.Net.xUnit.Microservice.Test.Validations;
/// <summary>/// A class for validating aggregates./// </summary>public class AggregateTest{ /// <summary> /// Validates that aggregates can be created using the constructor and their properties can be set and retrieved correctly. /// </summary> [Theory] [Aggregate<Errors>(false)] public void Aggregate_Constructor_ShouldSetAndRetrievePropertiesCorrectly(Type aggregate, object instance, Dictionary<ParameterInfo, object> values) { // Assert Assert.NotNull(instance);
var value = aggregate.GetProperty(nameof(AggregateRoot.Id))!.GetValue(instance, null); var valueExpected = values.First(x => x.Key.Name!.Equals(nameof(AggregateRoot.Id), StringComparison.OrdinalIgnoreCase)).Value; Assert.Equal(valueExpected, value); }
/// <summary> /// Validates that aggregates can be created using the named constructor with custom values. /// </summary> [Theory] [Aggregate<Errors>(true)] public void Aggregate_CreateMethod_ShouldCreateInstanceWithCustomValues(Type aggregate, object instance, Dictionary<ParameterInfo, object> values) { // Assert Assert.NotNull(instance);
var value = aggregate.GetProperty(nameof(AggregateRoot.Id))!.GetValue(instance, null); var valueExpected = values.First(x => x.Key.Name!.Equals(nameof(AggregateRoot.Id), StringComparison.OrdinalIgnoreCase)).Value; Assert.Equal(valueExpected, value); }}
En este ejemplo:
[Aggregate<Errors>(false)]
crea instancias de todos los agregados en el ensamblado donde se defineErrors
utilizando el constructor.[Aggregate<Errors>(true)]
crea instancias de todos los agregados en el ensamblado donde se defineErrors
utilizando el método estáticoCreate
, si existe.- Los métodos de prueba reciben el
Type
del agregado, lainstancia
del agregado y undiccionario
con los valores de los parámetros utilizados en la creación del mismo, lo que permite realizar aserciones sobre el objeto creado.
Conclusiones
El AggregateAttribute
es una herramienta valiosa para simplificar las pruebas unitarias de agregados en microservicios. Al automatizar la creación de instancias y la provisión de datos de prueba, este atributo permite a los desarrolladores concentrarse en la lógica de negocio y garantizar la calidad de su código.