Convention-based Customizations with AutoFixture by Mark Seemann
As previous posts have described, AutoFixture creates Anonymous Variables based on the notion of being able to always hit within a well-behaved Equivalence Class for a given type. This works well a lot of the time because AutoFixture has some sensible defaults: numbers are positive integers and strings are GUIDs.
This last part only works as long as strings are nothing but opaque blobs to the consuming class. This is, however, not an unreasonable assumption. Consider classes that implement Entities such as Person or Address. Strings will often take the form of FirstName, LastName, Street, etc. In all such cases, the value of the string usually doesn't matter.
However, there will always be cases where the value of a string has a special meaning of its own. It will often be best to let AutoFixture guide us towards a better API design, but this is not always possible. Sometimes there are rules that constrain the formatting of a string.
As an example, consider a Money class with this constructor:
public Money(decimal amount, string currencyCode)
{
if (currencyCode == null)
{
throw new ArgumentNullException("...");
}
if (!CurrencyCodes.IsValid(currencyCode))
{
throw new ArgumentException("...");
}
this.amount = amount;
this.currencyCode = currencyCode;
}
Notice that the constructor only allows properly formatted currency codes (such as e.g. “DKK”, “USD”, “AUD”, etc.) through, while other strings will throw an exception. AutoFixture's default behavior of creating GUIDs as strings is problematic, as the Money constructor will throw on a GUID.
We could attempt to fix this by changing the way AutoFixture generates strings in general, but that may not be the best solution as it may interfere with other string values. It is, however, easy to do:
fixture.Inject("DKK");
This simply injects the “DKK” string into the fixture, causing all subsequent strings to have the same value. However, a hypothetical Pizza class with Name and Description properties in addition to a Price property of type Money will now look like this:
{
"Name": "DKK",
"Price": {
"Amount": 1.0,
"CurrencyCode": "DKK"
},
"Description": "DKK"
}
What we really want is to customize only the currency code. This is where the extremely customizable architecture of AutoFixture can help us. As the documentation explains, lots of different request will flow through the kernel's Chain of Responsibility to create a Money instance. To populate the two parameters of the Money constructor, two ParameterInfo requests will be issued - one for each parameter. We can take advantage of this to create a custom ISpecimenBuilder that only addresses string parameters with the name currencyCode.
public class CurrencyCodeSpecimenBuilder :
ISpecimenBuilder
{
public object Create(object request,
ISpecimenContext context)
{
var pi = request as ParameterInfo;
if (pi == null)
{
return new NoSpecimen(request);
}
if (pi.ParameterType != typeof(string)
|| pi.Name != "currencyCode")
{
return new NoSpecimen(request);
}
return "DKK";
}
}
It simply examines the request to determine whether this is something that it should address at all. Only if the request is a ParameterInfo representing a string parameter named currencyCode do we deal with it. In any other case we return NoSpecimen, which simply tells AutoFixture that it should ask another ISpecimenBuilder instead.
Here we just return the hard-coded string “DKK”, but we could easily have expanded the example to use a more varied generation algorithm. I will leave that, as well as how to generalize this in other ways, as exercises to the reader.
With CurrencyCodeSpecimenBuilder available, we can add it to the Fixture like this:
fixture.Customizations.Add(
new CurrencyCodeSpecimenBuilder());
With this customization added, a Pizza instance now looks like this:
{
"Name": "namec6b7a923-ea78-4817-9e24-a6863a597645",
"Price": {
"Amount": 1.0,
"CurrencyCode": "DKK"
},
"Description": "Description63ef17d7-876d-46d8-af73-1ed91f83e699"
}
Notice how only the currency code is affected while all other string values are created by the default algorithm.
In a nutshell, a custom ISpecimenBuilder can be used to implement all sorts of custom conventions for AutoFixture. The one shown here applies the string “DKK” to all string parameters named currencyCode. This mean that the convention isn't necessarily constrained to the Money constructor, but applies to all ParamterInfo instances that fit the specification.
Comments
Thanks
The reason why the CurrencyCodeSpecimenBuilder is looking for a ParameterInfo instance is because the thing it's looking for is exactly the constructor parameter to the Money class.
If you instead want to match on a property, PropertyInfo is indeed the correct request to look for.
(and FieldInfo is used if you want to match on a public field...)
A request can be anything, but will often by a Type, ParameterInfo, or PropertyInfo.
You can use the
context
argument passed to the Create method to resolve other values; you only need to watch out for infinite recursions: you can't ask for an unconditional string if the Specimen Builder you're writing handles unconditional strings.I've written several custom ISpecimenBuilder implementations similar to your example above, and I always wince when testing for the ParameterType.Name value (i.e., if(pi.Name == "myParamName"){...}). It seems like it makes a test that would use this implementation very brittle - no longer would I have freedom to change the name of the paramter to suit my asthetics, without relying on a refactoring tool (cough, cough, Resharper, cough, cough) to hopefully pickup on the string value in my test suite and prompt me to change it there as well.
This makes me think that I shouldn't need to be doing this, and that a design refactoring of my SUT would be a better option. Care to comment on this observation? Is there a common scenario/design trap that illustrates a better way? Or am I already in dangerous design territory based on my need to create an ISpecimenBuilder in the first place?
Jeff, thank you for writing. Your question warranted a new blog post; it may not answer all of your concerns, but hopefully some of them. Read it and let me know if you still have questions.