对某些类型使用自动混合构建方法时,如何限制为填充对象字符串属性/字段而生成的字符串的长度?
答案 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这样的功能测试框架所做的事情,然后让您“缩减”价值。