当对某种类型使用 AutoFixture 的 Build 方法时,如何限制生成的字符串的长度以填充该对象的字符串属性/字段?
9 回答
使用该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));
如果最大长度是一个约束并且您拥有该类型的源代码,则可以使用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();
这是一个样本生成器,它可以生成任意长度的随机字符串——甚至比默认情况下的 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();
这是我的解决方案。当字符串包含的内容无关紧要时,我正在使用这种方法:
public static string GetStringOfLength(this IFixture fixture, int length)
{
return string.Join("", fixture.CreateMany<char>(length));
}
它很短,对我有用。
我在我的项目中添加了一个自定义字符串生成器。它附加一个 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();
}
}
其他一些解决方案非常好,但是如果您在基于数据模型的测试夹具中生成对象,您会遇到其他问题。首先,StringLength 属性对于代码优先数据模型来说不是一个很好的选择,因为它添加了看似重复的注释。为什么需要 StringLength 和 MaxLength 并不明显。手动保持它们同步是相当多余的。
我倾向于自定义 Fixture 的工作方式。
1) 您可以为一个类自定义夹具,并指定在创建该属性时,根据需要截断字符串。因此,要将 MyClass 中的 FieldThatNeedsTruncation 截断为 10 个字符,您可以使用以下命令:
fixture.Customize<MyClass>(c => c
.With(x => x.FieldThatNeedsTruncation, Fixture.Create<string>().Substring(0,10));
2)第一个解决方案的问题是您仍然需要保持长度同步,只是现在您可能在两个完全不同的类中而不是在两行连续的数据注释中这样做。
我想出的从任意数据模型生成数据而无需在您声明的每个自定义项中手动设置的第二个选项是使用直接评估 MaxLengthAttribute 的自定义 ISpecimenBuilder。这是我从库本身修改的类的源代码,它正在评估 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());
注意:这个解决方案并没有真正使用 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)");
}
}
这是我的解决方案和注释。
首先,很明显 AutoFixture.Create 中存在一些紧密耦合,以了解如何构建和定制样本。对于字符串,这很烦人,因为我们知道默认值是 Guid。使用这些知识,我创建了一个 Func 来在我的测试用例中处理这个问题:
private readonly Func<IFixture, int, string> _createString = (IFixture fixture, int length) => (fixture.Create<string>() + fixture.Create<string>()).Substring(0, length);
这可以归纳定义为利用 Auto-Fixture 默认生成的 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 这样的功能测试框架所做的,然后让你“缩小”价值。
您可以使用StringLength
属性:
public class MyData
{
[System.ComponentModel.DataAnnotations.StringLength(42)]
public string Description { get; set; }
}
然后像往常一样使用fixture
var fixture = new Fixture();
var mockData = fixture.Create<MyData>();