Skip to content

Domain Event

El DomainEventAttribute es un atributo personalizado diseñado para simplificar la creación de pruebas unitarias que validen el comportamiento de los eventos de dominio en tu aplicación. Este atributo automatiza la generación de instancias de eventos de dominio, ya sea utilizando el constructor o un método estático Create, lo que facilita la escritura de pruebas exhaustivas y reduce la repetición de código.

¿Cómo Funciona?


El DomainEventAttribute funciona como un proveedor de datos para las pruebas unitarias utilizando xUnit. Cuando se aplica a un método de prueba, este atributo realiza las siguientes acciones:

  1. Escanear el ensamblado: Identifica todas las clases que implementan la interfaz IDomainEvent, que es un marcador típico para eventos de dominio.
  2. Crear instancias: Para cada evento de dominio encontrado, crea una instancia utilizando el constructor o el método estático Create (si existe).
  3. Proveer datos: Suministra el tipo del evento de dominio, la instancia del evento y un diccionario de los valores utilizados para crear la instancia a la prueba unitaria.

Métodos


GetData

GetData(MethodInfo testMethod)

Este método es el núcleo del DomainEventAttribute. Se encarga de realizar las siguientes acciones:

  • Localizar Eventos de Dominio: Busca en el ensamblado especificado por el parámetro genérico TAssemblyScan todas las clases que cumplen con las siguientes condiciones:
    • Implementan la interfaz IDomainEvent.
    • No son una interfaz o clase abstracta.
  • Crear Instancias: Para cada evento de dominio encontrado:
    • Método Create: Si el parámetro useCreateMethod es true, se busca un método estático llamado Create en la clase del evento de dominio. Si no se encuentra, se lanza una excepción.
    • Constructor: Si el parámetro useCreateMethod es false o no se encuentra el método Create, se utiliza el constructor por defecto del evento de dominio.
    • Genera un diccionario de valores predeterminados para los parámetros del constructor usando el método GetParameterValues().
    • Crea una instancia del evento de dominio utilizando el método Invoke correspondiente (constructor o método Create) y los valores de los parámetros.
  • Retornar Datos: Retorna una secuencia de arrays de objetos, cada array contiene:
    • El Type del evento de dominio.
    • La instancia del evento de dominio.
    • Un diccionario con los ParameterInfo y los valores utilizados en la creación del evento.

Implementación


El DomainEventAttribute 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 eventos de dominio y un parámetro booleano useCreateMethod que indica si debe utilizarse el método Create o el constructor para crear la instancia del evento.

namespace CodeDesignPlus.Net.xUnit.Microservice.Attributes;
/// <summary>
/// A custom attribute for providing data to test methods that validate domain events.
/// </summary>
/// <typeparam name="TAssemblyScan">The type of the assembly to scan for domain events.</typeparam>
/// <param name="useCreateMethod">Indicates whether to use the static Create method or the constructor to create instances of domain events.</param>
[AttributeUsage(AttributeTargets.Method)]
public class DomainEventAttribute<TAssemblyScan>(bool useCreateMethod) : DataAttribute
{
/// <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 domainEvents = typeof(TAssemblyScan).Assembly
.GetTypes()
.Where(x => !x.FullName.StartsWith("Castle") || !x.FullName.Contains("DynamicProxyGenAssembly"))
.Where(x => typeof(IDomainEvent).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)
.ToList();
foreach (var domainEvent in domainEvents)
{
object instance;
Dictionary<ParameterInfo, object> values;
if (useCreateMethod)
{
var nameConstructor = domainEvent.GetMethod("Create", BindingFlags.Static | BindingFlags.Public);
if (nameConstructor is null)
throw new CoreException($"The {domainEvent.Name} class does not have a static Create method.");
values = nameConstructor.GetParameters().GetParameterValues();
instance = nameConstructor.Invoke(null, [.. values.Values]);
}
else
{
var constructor = domainEvent.GetConstructors().FirstOrDefault()!;
values = constructor.GetParameters().GetParameterValues();
instance = constructor.Invoke([.. values.Values]);
}
yield return new object[] { domainEvent, instance, values };
}
}
}

Ejemplo de Uso


El siguiente ejemplo muestra cómo utilizar el DomainEventAttribute en un método de prueba unitaria:

namespace CodeDesignPlus.Net.xUnit.Microservice.Test.Validations;
/// <summary>
/// A class for validating domain events.
/// </summary>
public class DomainEventTest
{
/// <summary>
/// Validates that domain events can be created using the constructor and their properties can be set and retrieved correctly.
/// </summary>
[Theory]
[DomainEvent<Errors>(false)]
public void DomainEvent_Constructor_ShouldSetAndRetrievePropertiesCorrectly(Type domainEvent, object instance, Dictionary<ParameterInfo, object> data)
{
// Assert
Assert.NotNull(instance);
Assert.All(domainEvent.GetProperties(), property =>
{
var value = property.GetValue(instance);
var valueExpected = data.FirstOrDefault(x => x.Key.Name!.Equals(property.Name, StringComparison.OrdinalIgnoreCase)).Value;
Assert.Equal(valueExpected, value);
});
}
/// <summary>
/// Validates that domain events can be created using the named constructor with custom values.
/// </summary>
[Theory]
[DomainEvent<Errors>(true)]
public void DomainEvent_CreateMethod_ShouldCreateInstanceWithCustomValues(Type domainEvent, object instance, Dictionary<ParameterInfo, object> values)
{
// Assert
Assert.NotEmpty(values);
Assert.NotNull(instance);
var property = domainEvent.GetProperty(nameof(DomainEvent.AggregateId));
var value = property!.GetValue(instance, null);
var valueExpected = property.PropertyType.GetDefaultValue();
Assert.NotEqual(valueExpected, value);
}
}

En este ejemplo:

  • [DomainEvent<Errors>(false)] crea instancias de todos los eventos de dominio en el ensamblado donde se define Errors utilizando el constructor.
  • [DomainEvent<Errors>(true)] crea instancias de todos los eventos de dominio en el ensamblado donde se define Errors utilizando el método estático Create, si existe.
  • Los métodos de prueba reciben el Type del evento de dominio, la instancia del evento de dominio y un diccionario con los valores de los parámetros utilizados en la creación del mismo, lo que permite realizar aserciones sobre el objeto creado.
  • La prueba verifica que los valores de las propiedades del evento de dominio coincidan con los valores esperados.
  • En el caso del metodo create verifica que se asigne un valor a la propiedad AggregateId que no es el valor por defecto.

Conclusiones


El DomainEventAttribute es una herramienta muy útil para simplificar la creación y validación de eventos de dominio en pruebas unitarias. Al automatizar la creación de instancias y la provisión de datos de prueba, este atributo permite a los desarrolladores garantizar la calidad de su lógica de negocio relacionada con los eventos de dominio.

Referencias Adicionales