一部のタイプにautofixtureビルドメソッドを使用する場合、そのオブジェクトの文字列プロパティ/フィールドを満たすために生成される文字列の長さをどのように制限できますか?
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
メソッドを使用します。なぜなら、私は代わりに慣習ベースのアプローチを好むからです。これを行うには、文字列の長さを制限する方法が少なくとも3つあります。
最初のオプションは、すべての文字列のベースを制約することです。
fixture.Customizations.Add(
new StringGenerator(() =>
Guid.NewGuid().ToString().Substring(0, 10)));
var mc = fixture.Create<MyClass>();
上記のカスタマイズでは、生成されたすべての文字列が10文字に切り捨てられます。ただし、デフォルトのプロパティ割り当てアルゴリズムはプロパティの名前を文字列の前に付加するため、最終結果は次のようになりますmc.SomeText
には "SomeText3c12f144-5"のような値が含まれるため、ほとんどの場合それはおそらく望みのものではありません。
別のオプションは、[StringLength]
属性、ニコスが指摘するように:
public class MyClass
{
[StringLength(10)]
public string SomeText { get; set; }
}
これは、プロパティの長さについて明示的に何も指定せずに、インスタンスを作成できることを意味します。
var mc = fixture.Create<MyClass>();
私が考えることができる3番目のオプションは私のお気に入りです。これにより、「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();
プロジェクトにカスタム文字列ビルダーを追加しました。 GUIDの代わりに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();
}
}
他のソリューションのいくつかはかなり良いですが、データモデルに基づいてテストフィクスチャでオブジェクトを生成している場合、遭遇する他の問題があります。まず、StringLength属性は、一見重複しているように見える注釈を追加するため、コードファーストデータモデルに適したオプションではありません。 StringLengthとMaxLengthの両方が必要な理由はすぐにはわかりません。手動で同期を維持することはかなり冗長です。
Fixtureの動作をカスタマイズすることに傾倒します。
1)クラスのフィクスチャをカスタマイズし、そのプロパティを作成するときに、必要に応じて文字列を切り捨てることを指定できます。したがって、MyClassのFieldThatNeedsTruncationを10文字に切り捨てるには、次のようにします。
fixture.Customize<MyClass>(c => c
.With(x => x.FieldThatNeedsTruncation, Fixture.Create<string>().Substring(0,10));
2)最初のソリューションの問題は、引き続き長さを同期させる必要があることです。今はおそらく、2行の連続するデータ注釈ではなく、2つの完全に異なるクラスでそれを実行しています。
宣言するカスタマイズごとに手動で設定せずに任意のデータモデルからデータを生成するために思いついた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.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のような機能テストフレームワークが行うことの一種であり、その後、値を「縮小」できます。
注:このソリューションは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)");
}
}