AutoFixture - 配置夹具以限制字符串生成长度

时间:2012-04-12 14:04:48

标签: c# autofixture

对某些类型使用自动混合构建方法时,如何限制为填充对象字符串属性/字段而生成的字符串的长度?

8 个答案:

答案 0 :(得分:47)

使用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>();

我能想到的第三个选择是我的最爱。这增加了一个特定的目标约定,该约定规定每当要求fixture为名称为“SomeText”且类型为string的属性创建值时,结果字符串应为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));

答案 1 :(得分:12)

如果最大长度是一个约束,并且您拥有该类型的源代码,则可以使用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();

答案 2 :(得分: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();

答案 3 :(得分:3)

这是我的解决方案。 当字符串包含的内容无关紧要时,我将使用此方法:

public static string GetStringOfLength(this IFixture fixture, int length)
    {
        return string.Join("", fixture.CreateMany<char>(length));
    }

它很短,对我有用。

答案 4 :(得分:1)

我在项目中添加了自定义字符串构建器。它附加一个4位数字而不是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();
        }
    }

答案 5 :(得分:1)

其他一些解决方案非常好,但如果您根据数据模型在测试夹具中生成对象,则还会遇到其他问题。首先,StringLength属性对于代码优先数据模型来说不是一个很好的选择,因为它添加了看似重复的注释。很明显为什么你需要StringLength和MaxLength。手动保持同步是相当多余的。

我倾向于定制灯具的工作原理。

1)您可以自定义类的fixture,并指定在创建该属性时,根据需要截断字符串。因此,要将MyClass中的FieldThatNeedsTruncation截断为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 :(得分:0)

注意:此解决方案并没有真正使用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 :(得分:0)

这是我的解决方法和注意事项。

首先,很明显AutoFixture中存在紧密的耦合。创建知识以了解如何构建和定制标本。对于字符串,这很烦人,因为我们知道默认值为Guid。利用这些知识,我创建了一个在测试用例中处理此问题的Func:

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

这可以归纳为默认情况下利用自动修复生成的GUID。默认情况下,它是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这样的功能测试框架所做的事情,然后让您“缩减”价值。