The Scenario
I’ve been using AutoFixture on and off for years but got back into it quite heavily recently. Using it with the Xunit extension points and [Theory]
attribute makes writing unit tests a joy.
The default settings for most types has been more than enough for most things but recently I’ve had the requirement that an external id string needs to be in a specific format. Due to the way AutoFixture creates string the default setup would not work.
Can this be done? How can it be done? Let’s find out.
Where do we start
Like most code starting with a unit test to show what we are trying to solve is a good place to begin.
The problem is all around the external id of a POCO (Plain Old C# Object) like the below.
public class Person
{
public string ExternalId { get; set; }
}
As you can see there is nothing fancy about it. However there is a business requirement which says the external id has a specific format.
[Fact]
public void ExternalId_ShouldBe_Correct_Format()
{
// Arrange
var fixture = new Fixture();
// Act
var sut = fixture.Create<Person>();
// Assert
sut.ExternalId.Should().StartWith("123-").And.HaveLength(7);
var part = sut.ExternalId.Split('-')[1];
int.TryParse(part, out int value).Should().BeTrue();
value.Should().BeInRange(100, 999);
}
As we can see from the unit test there are a number of business rules as to the format of the identifier.
- Needs to start ‘123-’
- Should be 7 characters long
- The part after the ‘123-’ needs to be a valid int
- The valid int needs to be between 100 and 999.
This is just a simple example but as you can see there is already a number of items to check. Now the default value of string properties when generated by AutoFixture is the format of “name of the property + guid”. This will not do for the requirements.
Specimen Builder to the Rescue
AutoFixture does a lot of things out of the box however it also allows for items to be customised. This can either be done through ICustomization
implementations or ISpecimenBuilder
implementations.
The differences are ICustomizations are usually addressing a larger issue such as dynamically creating mocked objects which depend on each other. Where as a Specimen builder can be as broad or as targetted as you like. Both are added to the AutoFixture IFixture
instance and used when generating the instances requested.
To generate the specific format we have to intercept at the correct point at which the value is needed. This is done through reflection.
Breaking it down
First off we need to generate random numbers so need some sort of number generator. We create this in the constructor in a similar fashion to the AutoFixture source does.
private readonly Random _random;
public PersonExternalIdGenerator()
{
_random = new Random();
}
Side note: Highly recommend cloning the repository of any open source library that you use and take a dive in to get a better understanding of how it works. I was struggling with finding examples and documentation online when researching this so went to the code and found pointers all over it!
Next up is the Create
method. This is the only method you are required to implement when implementing ISpecimenBuilder
. It expects a request to be passed in with a context. As you will notice context is specified as object
. This is due to the fact that depending on where the dependency graph this is called object could be “anything” (within reason).
We need to check if the request is the one we are wanting to intercept.
if (request is PropertyInfo pi)
{
if (pi.DeclaringType == typeof(Person)
&& pi.PropertyType == typeof(string)
&& pi.Name == nameof(Person.ExternalId))
We now know that the request is of type PropertyInfo
. This allows us to inspect and interogate the information about the property we’re looking at. I’ve broken down this if into 2 statements to be clearer but you can combine these or split these down further if you require.
We need to check that the DeclaringType which we are looking at is the target type. If not then we can ignore it. We then check that the type of the property is string and then finally the name of the property is the one we’re targetting. It is good practice to use the nameof()
operator here to avoid “magic” strings. If a string representation of “ExternalId” was used and then the property name was changed through refactoring this would not get updated and could cause issues.
If all the conditions are satisfied we now get to create our new external id.
return $"001-{_random.Next(100, 999)}";
And finally if we’ve not satisfied the requirements we need to return a new NoSpecimen
so that AutoFixture knows it needs to carry on through it’s generators to find an appropriate sample value for the request.
Full implementation
With the breakdown let’s take a look at the full implementation.
public class PersonExternalIdGenerator : ISpecimenBuilder
{
private readonly Random _random;
public PersonExternalIdGenerator()
{
_random = new Random();
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo pi)
{
if (pi.DeclaringType == typeof(Person)
&& pi.PropertyType == typeof(string)
&& pi.Name == nameof(Person.ExternalId))
{
return $"001-{_random.Next(100, 999)}";
}
}
return new NoSpecimen();
}
}
Consuming the specimen builder
Now we have our specimen builder we need to consume it.
[Fact]
public void ExternalId_ShouldBe_Correct_Format()
{
// Arrange
var fixture = new Fixture();
fixture.Customizations.Add(new PersonExternalIdGenerator());
// Act
var sut = fixture.Create<Person>();
// Assert
sut.ExternalId.Should().StartWith("123-").And.HaveLength(7);
var part = sut.ExternalId.Split('-')[1];
int.TryParse(part, out int value).Should().BeTrue();
value.Should().BeInRange(100, 999);
}
As you can see the only difference from the original unit test is the addition of our new PersonExternalIdGenerator
specimen builder being added into the Customizations
list on our AutoFixture Fixture instance.
Conclusion
In this post we have reviewed custom formatting of properties which are specified through business requirements and how to generate them through using AutoFixture specimen builders to aid with unit testing.
The other way of doing this would be to manually generate the specified string each test or create a “helper” or “utiltity” method which would do it for you. In my opinion this would not be as clean and you would have to remember to use it each time. Using this methodology the required property on the specific POCO is set every time you request an intance.
Any questions/comments then please contact me on Twitter @WestDiscGolf