web-dev-qa-db-fra.com

Autofixture - Configurer le luminaire pour limiter la longueur de la génération de chaîne

Lors de l'utilisation de la méthode de construction Autofixture pour un type, comment puis-je limiter la longueur des chaînes générées pour remplir les propriétés de la chaîne d'objets/champs?

40
Ricardo Rodrigues

Avec la méthode Build, il n'y a pas beaucoup d'options, mais vous pouvez faire quelque chose comme ceci:

var constrainedText = 
    fixture.Create<string>().Substring(0, 10);
var mc = fixture
    .Build<MyClass>()
    .With(x => x.SomeText, constrainedText)
    .Create();

Cependant, personnellement, je ne vois pas comment cela est meilleur ou plus facile à comprendre que ceci:

var mc = fixture
    .Build<MyClass>()
    .Without(x => x.SomeText)
    .Create();
mc.SomeText =
    fixture.Create<string>().Substring(0, 10);

Personnellement, je très rarement Utilisez la méthode Build, puisque je préfère une approche basée sur la convention à la place. Faire cela, il y a au moins trois façons de contraindre la longueur de la chaîne.

La première option est juste de contraindre la base de toutes les cordes:

fixture.Customizations.Add(
    new StringGenerator(() =>
        Guid.NewGuid().ToString().Substring(0, 10)));
var mc = fixture.Create<MyClass>();

La personnalisation ci-dessus tronque toutes les chaînes générées à 10 caractères. Cependant, étant donné que l'algorithme d'affectation de propriété par défaut achète le nom de la propriété à la chaîne, le résultat final sera que mc.SomeText aura une valeur comme "singext3c12f144-5", ce qui n'est probablement pas ce que vous voulez la plupart du temps.

Une autre option consiste à utiliser le [StringLength] Attribut, comme indique Nikos:

public class MyClass
{
    [StringLength(10)]
    public string SomeText { get; set; }
}

Cela signifie que vous pouvez simplement créer une instance sans rien dire explicitement de la longueur de la propriété:

var mc = fixture.Create<MyClass>();

La troisième option que je peux penser est mon préféré. Cela ajoute une convention spécifiquement ciblée qui indique que chaque fois que le luminaire est invité à créer une valeur pour une propriété avec le nom "Mémoriser" et de la chaîne de type, la chaîne résultante doit comporter exactement 10 caractères:

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

Usage:

fixture.Customizations.Add(new SomeTextBuilder());
var mc = fixture.Create<MyClass>();

La beauté de cette approche est qu'elle laisse la SUT seule et n'affecte toujours aucune autre valeur de chaîne.


Vous pouvez généraliser ceci SpecimenBuilder à une classe et de la longueur, comme si:

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

Usage:

fixture.Customizations.Add(
    new StringPropertyTruncateSpecimenBuilder<Person>(p => p.Initials, 5));
54
Mark Seemann

Si la longueur maximale est une contrainte et que vous possédez le code source pour le type, vous pouvez utiliser la classe StringLengthateTtribute pour spécifier la longueur maximale des caractères autorisés.

À partir de la version 2.6.0, Autofixture prend en charge les donnéesAnotations et générera automatiquement une chaîne avec la longueur maximale spécifiée.

Par exemple,

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
}

Le test ci-dessus passera également lors de l'utilisation de Build (pour personnaliser l'algorithme de création pour un seul objet):

var result = fixture.Build<StringLengthValidatedType>().CreateAnonymous();
12
Nikos Baxevanis

Voici un constructeur de spécimen qui peut générer des chaînes aléatoires de longueur arbitraire - même plus longtemps que les chaînes GUID + Nom de propriété qui sont par défaut. En outre, vous pouvez choisir le sous-ensemble des caractères que vous souhaitez utiliser et même passer de votre propre aléatoire (afin que vous puissiez contrôler la graine si vous devez)

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

Vous pouvez ensuite l'utiliser pour peupler une propriété d'un objet comme celui-ci:

        var input = _fixture.Build<HasAccountNumber>()
                            .With(x => x.AccountNumber,
                                  new SpecimenContext(new RandomStringOfLengthGenerator())
                                      .Resolve(new RandomStringOfLengthRequest(50)))
                            .Create();
3
Peter McEvoy

J'ai ajouté un constructeur de cordes personnalisé à mon projet. Il ajoute un numéro à 4 chiffres au lieu d'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();
        }
    }
1
CSharper

Certaines des autres solutions sont plutôt bonnes, mais si vous générez des objets dans un appareil de test basé sur un modèle de données, il existe d'autres problèmes que vous rencontrez. Premièrement, l'attribut StringLength n'est pas une excellente option pour un modèle de code-première-modèle, car il ajoute des annotations apparemment en double. Ce n'est pas facilement évident pourquoi vous avez besoin à la fois de longueur de source et de maxlength. Les garder en synchronisation manuellement est plutôt redondant.

Je me pencherais vers la personnalisation de la façon dont le luminaire fonctionne.

1) Vous pouvez personnaliser le luminaire pour une classe et préciser que lors de la création de cette propriété, vous tronquez la chaîne, au besoin. Donc, pour tronquer le terrain de terrain dans la myclass à 10 caractères, vous utiliseriez ce qui suit:

fixture.Customize<MyClass>(c => c
  .With(x => x.FieldThatNeedsTruncation, Fixture.Create<string>().Substring(0,10));

2) Le problème avec la première solution est que vous devez toujours conserver la longueur de synchronisation, que vous ne le faites probablement que dans deux classes totalement différentes plutôt que dans deux lignes d'annotations de données consécutives.

La deuxième option que j'ai proposée pour générer des données à partir d'un modèle de données arbitraire sans avoir à la définir manuellement dans chaque personnalisation que vous déclarez avoir d'utiliser un ispecimenbuilder personnalisé qui évalue directement le maxlengthTtribute directement. Voici le code source d'une classe que j'ai modifiée de la bibliothèque elle-même, qui évaluait la frigorgthatettribute.

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

Ensuite, ajoutez-le simplement comme une personnalisation, comme suit:

fixture.Customizations.Add(new MaxLengthAttributeRelay());
1
Mike Taber

Voici ma solution et Notes.

Premièrement, il est clair qu'il existe un couplage serré dans la mise à l'autofixture.Create à la connaissance de la manière dont un spécimen est construit et personnalisé. Pour les chaînes, c'est ennuyeux parce que nous savons que la valeur par défaut est une solution. En utilisant ces connaissances, j'ai créé un fonctionnement qui gère cela dans mes cas de test:

private readonly Func<IFixture, int, string> _createString = (IFixture fixture, int length) => (fixture.Create<string>() + fixture.Create<string>()).Substring(0, length);

Cela pourrait être défini de manière inductive pour exploiter le GUID généré par l'auto-luminaire par défaut. Il est de 36 caractères par défaut, donc:

private readonly Func<IFixture, int, string> _createString = (IFixture fixture, int length) =>
        {
            if (length < 0) throw new ArgumentOutOfRangeException(nameof(length));
            var sb = new StringBuilder();
            const int autoFixtureStringLength = 36;
            var i = length;
            do
            {
                sb.Append(fixture.Create<string>());
                i -= autoFixtureStringLength;
            } while (i > autoFixtureStringLength && i % autoFixtureStringLength > 0);
            sb.Append(fixture.Create<string>());
            return (sb).ToString().Substring(0, length);
        };

Encore une fois, toute la prémisse à cette solution est que l'autofixation est déjà étroitement couplée à la politique de création d'objet que vous avez. Tout ce que vous faites, c'est colomber sur cela.

Il serait probablement idéal si l'autofixation a exposé une "valeur minute" et "valeur maxe" point d'extension à la requête. C'est une sorte de quels cadres de test fonctionnels comme QuickCheck font, puis vous permettent de "réduire" la valeur.

0
John Zabroski

Remarque: cette solution n'utilise pas vraiment l'autofixture, mais il est parfois difficile d'utiliser le colis, alors de le programmer vous-même.

Pourquoi utiliser AF quand il est plus difficile et Ulier d'utiliser AF, mon utilisation préférée est la suivante:

var fixture = new Fixture();
fixture.Create<string>(length: 9);

J'ai donc créé une méthode d'extension:

public static class FixtureExtensions
{
    public static T Create<T>(this IFixture fixture, int length) where T : IConvertible, IComparable, IEquatable<T>
    {
        if (typeof(T) == typeof(string))
        {
            // there are some length flaws here, but you get the point.
            var value = fixture.Create<string>();

            if (value.Length < length)
                throw new ArgumentOutOfRangeException(nameof(length));

            var truncatedValue = value.Substring(0, length);
            return (T)Convert.ChangeType(truncatedValue, typeof(T));
        }

        // implement other types here

        throw new NotSupportedException("Only supported for strings (for now)");
    }
}
0
Nick N.