AutoFixture - configure fixture to limit string generation length
When using AutoFixture's Build method for some type, how can I limit the length of the strings generated to fill that object's string properties/fields?
Solution 1:
With the Build
method itself, there aren't that many options, but you can do something like this:
var constrainedText =
fixture.Create<string>().Substring(0, 10);
var mc = fixture
.Build<MyClass>()
.With(x => x.SomeText, constrainedText)
.Create();
However, personally, I don't see how this is any better or easier to understand that this:
var mc = fixture
.Build<MyClass>()
.Without(x => x.SomeText)
.Create();
mc.SomeText =
fixture.Create<string>().Substring(0, 10);
Personally, I very rarely use the Build
method, since I prefer a convention-based approach instead. Doing that, there are at least three ways to constrain string length.
The first option is just to constrain the base of all strings:
fixture.Customizations.Add(
new StringGenerator(() =>
Guid.NewGuid().ToString().Substring(0, 10)));
var mc = fixture.Create<MyClass>();
The above customization truncates all generated strings to 10 characters. However, since the default property assignment algorithm prepends the name of the property to the string, the end result will be that mc.SomeText
will have a value like "SomeText3c12f144-5", so that is probably not what you want most of the time.
Another option is to use the [StringLength]
attribute, as Nikos points out:
public class MyClass
{
[StringLength(10)]
public string SomeText { get; set; }
}
This means that you can just create an instance without explicitly stating anything about the property's length:
var mc = fixture.Create<MyClass>();
The third option I can think of is my favorite. This adds a specifically targeted convention that states that whenever the fixture is asked to create a value for a property with the name "SomeText" and of type string, the resulting string should be exactly 10 characters long:
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();
}
}
Usage:
fixture.Customizations.Add(new SomeTextBuilder());
var mc = fixture.Create<MyClass>();
The beauty of this approach is that it leaves the SUT alone and still doesn't affect any other string values.
You can generalize this SpecimenBuilder
to any class and length, like so:
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;
}
}
Usage:
fixture.Customizations.Add(
new StringPropertyTruncateSpecimenBuilder<Person>(p => p.Initials, 5));
Solution 2:
If maximum length is a constraint and you own the source code for the type, you can use the StringLengthAttribute class to specify the maximum length of characters that are allowed.
From version 2.6.0, AutoFixture supports DataAnnotations and it will automatically generate a string with the maximum length specified.
As an example,
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
}
The above test will also pass when using Build (to customize the creation algorithm for a single object):
var result = fixture.Build<StringLengthValidatedType>().CreateAnonymous();
Solution 3:
Here's a specimen builder that can generate random strings of arbitrary length - even longer than the Guid+PropertyName strings that are by default. Also, you can choose the subset of chars you want to use and even pass in your own random (so that you can control the seed if you need to)
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();
}
}
You then can use it to populate a property of an object like this:
var input = _fixture.Build<HasAccountNumber>()
.With(x => x.AccountNumber,
new SpecimenContext(new RandomStringOfLengthGenerator())
.Resolve(new RandomStringOfLengthRequest(50)))
.Create();
Solution 4:
Here is my solution. When it doesn't matter what the string contains then I'm using this method:
public static string GetStringOfLength(this IFixture fixture, int length)
{
return string.Join("", fixture.CreateMany<char>(length));
}
It's short and it works for me.