c# - AutoFixture-configure accesorio para limitar la longitud de generación de cadenas
(5)
Cuando se utiliza el método de compilación autofixture para algún tipo, ¿cómo puedo limitar la longitud de las cadenas generadas para completar las propiedades / campos de las cadenas de objetos?
Agregué un generador de cadenas personalizado a mi proyecto. Añade un número de 4 dígitos en lugar de un guid.
public class StringBuilder : ISpecimenBuilder
{
private readonly Random rnd = new Random();
public object Create(object request, ISpecimenContext context)
{
var type = request as Type;
if (type == null || type != typeof(string))
{
return new NoSpecimen();
}
return rnd.Next(0,10000).ToString();
}
}
Algunas de las otras soluciones son bastante buenas, pero si está generando objetos en un dispositivo de prueba basado en un modelo de datos, hay otros problemas con los que se encontrará. Primero, el atributo StringLength no es una gran opción para un modelo de datos de primer código porque agrega anotaciones aparentemente duplicadas. No es fácil entender por qué necesita StringLength y MaxLength. Mantenerlos sincronizados manualmente es bastante redundante.
Me inclinaría por personalizar cómo funciona el dispositivo.
1) Puede personalizar el dispositivo para una clase y especificar que al crear esa propiedad, trunca la cadena, según sea necesario. Entonces, para truncar FieldThatNeedsTruncation en MyClass a 10 caracteres, usaría lo siguiente:
fixture.Customize<MyClass>(c => c
.With(x => x.FieldThatNeedsTruncation, Fixture.Create<string>().Substring(0,10));
2) El problema con la primera solución es que todavía necesita mantener la longitud sincronizada, solo que ahora es probable que lo haga en dos clases completamente diferentes en lugar de hacerlo en dos líneas de anotaciones de datos consecutivas.
La segunda opción que se me ocurrió para generar datos de un Modelo de Datos arbitrario sin tener que configurarlo manualmente en cada personalización que declare es usar un Especificador Específico personalizado que evalúe directamente el MaxLengthAttribute. Aquí está el código fuente de una clase que modifiqué de la biblioteca, que estaba evaluando StringLengthAttribute.
/// <summary>
/// Examine the attributes of the current property for the existence of the MaxLengthAttribute.
/// If set, use the value of the attribute to truncate the string to not exceed that length.
/// </summary>
public class MaxLengthAttributeRelay : ISpecimenBuilder
{
/// <summary>
/// Creates a new specimen based on a specified maximum length of characters that are allowed.
/// </summary>
/// <param name="request">The request that describes what to create.</param>
/// <param name="context">A container that can be used to create other specimens.</param>
/// <returns>
/// A specimen created from a <see cref="MaxLengthAttribute"/> encapsulating the operand
/// type and the maximum of the requested number, if possible; otherwise,
/// a <see cref="NoSpecimen"/> instance.
/// Source: https://github.com/AutoFixture/AutoFixture/blob/ab829640ed8e02776e4f4730d0e72ab3cc382339/Src/AutoFixture/DataAnnotations/StringLengthAttributeRelay.cs
/// This code is heavily based on the above code from the source library that was originally intended
/// to recognized the StringLengthAttribute and has been modified to examine the MaxLengthAttribute instead.
/// </returns>
public object Create(object request, ISpecimenContext context)
{
if (request == null)
return new NoSpecimen();
if (context == null)
throw new ArgumentNullException(nameof(context));
var customAttributeProvider = request as ICustomAttributeProvider;
if (customAttributeProvider == null)
return new NoSpecimen();
var maxLengthAttribute = customAttributeProvider.GetCustomAttributes(typeof(MaxLengthAttribute), inherit: true).Cast<MaxLengthAttribute>().SingleOrDefault();
if (maxLengthAttribute == null)
return new NoSpecimen();
return context.Resolve(new ConstrainedStringRequest(maxLengthAttribute.Length));
}
}
A continuación, simplemente agréguelo como una personalización, de la siguiente manera:
fixture.Customizations.Add(new MaxLengthAttributeRelay());
Aquí hay un generador de espécimen que puede generar cadenas aleatorias de longitud arbitraria, incluso más largas que las cadenas Guid + PropertyName que están predeterminadas. Además, puedes elegir el subconjunto de caracteres que quieras usar e incluso pasar tu propio elemento aleatorio (para que puedas controlar la semilla si es necesario)
public class RandomStringOfLengthRequest
{
public RandomStringOfLengthRequest(int length) : this(length, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890 !?,.-")
{
}
public RandomStringOfLengthRequest(int length, string charactersToUse): this(length, charactersToUse, new Random())
{
}
public RandomStringOfLengthRequest(int length, string charactersToUse, Random random)
{
Length = length;
Random = random;
CharactersToUse = charactersToUse;
}
public int Length { get; private set; }
public Random Random { get; private set; }
public string CharactersToUse { get; private set; }
public string GetRandomChar()
{
return CharactersToUse[Random.Next(CharactersToUse.Length)].ToString();
}
}
public class RandomStringOfLengthGenerator : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (request == null)
return new NoSpecimen();
var stringOfLengthRequest = request as RandomStringOfLengthRequest;
if (stringOfLengthRequest == null)
return new NoSpecimen();
var sb = new StringBuilder();
for (var i = 0; i < stringOfLengthRequest.Length; i++)
sb.Append(stringOfLengthRequest.GetRandomChar());
return sb.ToString();
}
}
Luego puede usarlo para poblar una propiedad de un objeto como este:
var input = _fixture.Build<HasAccountNumber>()
.With(x => x.AccountNumber,
new SpecimenContext(new RandomStringOfLengthGenerator())
.Resolve(new RandomStringOfLengthRequest(50)))
.Create();
Si la longitud máxima es una restricción y usted posee el código fuente para el tipo, puede usar la clase StringLengthAttribute para especificar la longitud máxima de caracteres permitidos.
A partir de la versión 2.6.0, AutoFixture admite DataAnnotations y generará automáticamente una cadena con la longitud máxima especificada.
Como ejemplo,
public class StringLengthValidatedType
{
public const int MaximumLength = 3;
[StringLength(MaximumLength)]
public string Property { get; set; }
}
[Fact]
public void CreateAnonymousWithStringLengthValidatedTypeReturnsCorrectResult()
{
// Fixture setup
var fixture = new Fixture();
// Exercise system
var result = fixture.CreateAnonymous<StringLengthValidatedType>();
// Verify outcome
Assert.True(result.Property.Length <= StringLengthValidatedType.MaximumLength);
// Teardown
}
La prueba anterior también pasará cuando use Build (para personalizar el algoritmo de creación para un solo objeto):
var result = fixture.Build<StringLengthValidatedType>().CreateAnonymous();
Con el método Build
sí mismo, no hay tantas opciones, pero puedes hacer algo como esto:
var constrainedText =
fixture.Create<string>().Substring(0, 10);
var mc = fixture
.Build<MyClass>()
.With(x => x.SomeText, constrainedText)
.Create();
Sin embargo, personalmente, no veo cómo esto sea mejor o más fácil de entender que esto:
var mc = fixture
.Build<MyClass>()
.Without(x => x.SomeText)
.Create();
mc.SomeText =
fixture.Create<string>().Substring(0, 10);
Personalmente, rara vez utilizo el método Build
, ya que prefiero un enfoque basado en convenciones. Al hacerlo, hay al menos tres formas de limitar la longitud de la cadena.
La primera opción es limitar la base de todas las cadenas:
fixture.Customizations.Add(
new StringGenerator(() =>
Guid.NewGuid().ToString().Substring(0, 10)));
var mc = fixture.Create<MyClass>();
La personalización anterior trunca todas las cadenas generadas a 10 caracteres. Sin embargo, dado que el algoritmo de asignación de propiedades predeterminado incluye el nombre de la propiedad en la cadena, el resultado final será que mc.SomeText
tendrá un valor como "SomeText3c12f144-5", por lo que probablemente no sea lo que quiera la mayoría de las veces .
Otra opción es usar el atributo [StringLength]
, como señala Nikos:
public class MyClass
{
[StringLength(10)]
public string SomeText { get; set; }
}
Esto significa que solo puede crear una instancia sin indicar explícitamente nada sobre la longitud de la propiedad:
var mc = fixture.Create<MyClass>();
La tercera opción que puedo pensar es mi favorita. Esto agrega una convención específica que establece que siempre que se solicite al dispositivo crear un valor para una propiedad con el nombre "SomeText" y de tipo string, la cadena resultante debe tener exactamente 10 caracteres de longitud:
public class SomeTextBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
var pi = request as PropertyInfo;
if (pi != null &&
pi.Name == "SomeText" &&
pi.PropertyType == typeof(string))
return context.Resolve(typeof(string))
.ToString().Substring(0, 10);
return new NoSpecimen();
}
}
Uso:
fixture.Customizations.Add(new SomeTextBuilder());
var mc = fixture.Create<MyClass>();
La belleza de este enfoque es que deja el SUT solo y aún no afecta a ningún otro valor de cadena.
Puedes generalizar este SpecimenBuilder
a cualquier clase y longitud, así:
public class StringPropertyTruncateSpecimenBuilder<TEntity> : ISpecimenBuilder
{
private readonly int _length;
private readonly PropertyInfo _prop;
public StringPropertyTruncateSpecimenBuilder(Expression<Func<TEntity, string>> getter, int length)
{
_length = length;
_prop = (PropertyInfo)((MemberExpression)getter.Body).Member;
}
public object Create(object request, ISpecimenContext context)
{
var pi = request as PropertyInfo;
return pi != null && AreEquivalent(pi, _prop)
? context.Create<string>().Substring(0, _length)
: (object) new NoSpecimen(request);
}
private bool AreEquivalent(PropertyInfo a, PropertyInfo b)
{
return a.DeclaringType == b.DeclaringType
&& a.Name == b.Name;
}
}
Uso:
fixture.Customizations.Add(
new StringPropertyTruncateSpecimenBuilder<Person>(p => p.Initials, 5));