Подтвердить что ты не робот

AutoFixture - настроить прибор, чтобы ограничить длину генерации строки

При использовании метода сборки автоопределения для некоторого типа, как я могу ограничить длину строк, сгенерированных для заполнения этих свойств/полей строки?

4b9b3361

Ответ 1

С помощью самого метода Build существует не так много параметров, но вы можете сделать что-то вроде этого:

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

Однако лично я не вижу, как это лучше или проще понять, что это:

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

Лично я очень редко использую метод Build, так как вместо этого я предпочитаю подход, основанный на соглашениях. При этом существует как минимум три способа ограничения длины строки.

Первый вариант - просто ограничить базу всех строк:

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

Вышеуказанная настройка обрезает все сгенерированные строки до 10 символов. Однако, поскольку алгоритм назначения свойств по умолчанию добавляет имя свойства в строку, конечный результат будет заключаться в том, что mc.SomeText будет иметь значение, подобное "SomeText3c12f144-5", так что это, вероятно, не то, что вы хотите большую часть времени.

Другой вариант - использовать атрибут [StringLength], как указывает Nikos:

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

Это означает, что вы можете просто создать экземпляр, явно не указав ничего о длине свойства:

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

Третий вариант, о котором я могу думать, - мой любимый. Это добавляет специально нацеленное соглашение, в котором говорится, что всякий раз, когда к устройству предлагается создать значение для свойства с именем "SomeText" и строкой типа, результирующая строка должна быть ровно 10 символов:

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

Использование:

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

Красота этого подхода заключается в том, что он оставляет только SUT и по-прежнему не влияет на другие строковые значения.


Вы можете обобщить этот SpecimenBuilder на любой класс и длину, например:

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

Использование:

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

Ответ 2

Если максимальная длина является ограничением и у вас есть исходный код для типа, вы можете использовать класс StringLengthAttribute, чтобы указать максимальную длину символов, которые допускается.

Начиная с версии 2.6.0, AutoFixture поддерживает DataAnnotations и автоматически генерирует строку с указанной максимальной длиной.

В качестве примера,

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
}

Вышеприведенный тест также пройдет при использовании Build (для настройки алгоритма создания для одного объекта):

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

Ответ 3

Здесь построит образец, который может генерировать случайные строки произвольной длины - даже дольше, чем строки Guid + PropertyName, которые по умолчанию. Кроме того, вы можете выбрать подмножество символов, которые хотите использовать, и даже пройти в своем собственном случайном порядке (чтобы вы могли контролировать семя, если вам нужно)

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

Затем вы можете использовать его для заполнения свойства объекта следующим образом:

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

Ответ 4

Я добавил в проект собственный построитель строк. Он добавляет 4-значное число вместо указателя.

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

Ответ 5

Некоторые из других решений довольно хороши, но если вы создаете объекты в тестовом устройстве на основе модели данных, есть и другие проблемы, с которыми вы столкнетесь. Во-первых, атрибут StringLength не является отличным вариантом для модели данных, основанной на кодах, поскольку он добавляет кажущиеся дубликаты аннотаций. Непонятно, почему вам нужны как StringLength, так и MaxLength. Сохранение их в синхронизации вручную является излишним.

Я бы наклонился к настройке того, как работает Fixture.

1) Вы можете настроить прибор для класса и указать, что при создании этого свойства вы обрезаете строку, если необходимо. Итак, чтобы обрезать FieldThatNeedsTruncation в MyClass до 10 символов, вы должны использовать следующее:

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

2) Проблема с первым решением заключается в том, что вам все равно нужно синхронизировать длину, только теперь, вероятно, вы делаете это в двух совершенно разных классах, а не в двух строках последовательных аннотаций данных.

Второй вариант, который я придумал для генерации данных из произвольной модели данных без необходимости вручную устанавливать его в каждой пользовательской настройке, которую вы объявляете, заключается в использовании настраиваемого ISpecimenBuilder, который напрямую вычисляет MaxLengthAttribute. Здесь исходный код для класса, который я модифицировал из самой библиотеки, которая оценивала атрибут 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));
    }
}

Затем просто добавьте его в качестве настройки, как показано ниже:

fixture.Customizations.Add(new MaxLengthAttributeRelay());

Ответ 6

Примечание. Это решение на самом деле не использует AutoFixture, но иногда сложнее использовать пакет, чем просто программировать его самостоятельно.

Зачем использовать AF, когда использовать AF труднее и уродливее, я предпочитаю использовать:

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

Поэтому я создал метод расширения:

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

Ответ 7

Вот мое решение и примечания.

Во-первых, ясно, что в AutoFixture есть некоторая тесная связь. Создайте знания о том, как образец создается и настраивается. Для строк это раздражает, потому что мы знаем, что по умолчанию используется Guid. Используя эти знания, я создал Func, который обрабатывает это в моих тестовых примерах:

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

Это может быть определено индуктивно, чтобы использовать guid, сгенерированный Auto-Fixture по умолчанию. По умолчанию это 36 символов, поэтому:

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

Опять же, вся предпосылка этого решения заключается в том, что AutoFixture уже тесно связана с любой политикой создания объектов, которая у вас есть. Все, что ты делаешь, это увлекаешься этим.

Вероятно, было бы идеально, если бы AutoFixture выставлял точку запроса "минимальное значение" и "максимальное значение" для запроса. Это то, что делают фреймворки функционального тестирования, такие как QuickCheck, и затем позволяют "уменьшить" значение.