Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AutoFixture and Custom DataAnnotations

Tags:

autofixture

I'm using AutoFixture in my unit and integration tests and ran into a problem. I'm generating data transfer objects and some of those classes have DataAnnotation attributes (some of which are custom) on the properties. AutoFixture sees those and doesn't generate data for them, presumably because it is unsure what data it is expecting.

An example of a custom validation attribute I'm using:

public class Person 
{
   public string FullName { get; set; }

   [Enum("M", "F")]
   public string Gender { get; set; }
}

public class EnumAttribute : ValidationAttribute
{
   public EnumAttribute(params string[] allowedStrings)
    {
        if (allowedStrings == null || allowedStrings.Length == 0)
            throw new ArgumentException("allowedStrings");
        AllowNull = true;
        AllowedStrings = allowedStrings;
        ErrorMessage = "The field '{0}' is invalid. Allowed strings are " + string.Join(", ", AllowedStrings);
    }

    // ... implementation
}

It just restricts a provided string to certain value (I couldn't use a straight up enum for other reasons).

How could I customize AutoFixture to create the appropriate data?

like image 775
JonH Avatar asked Aug 05 '14 20:08

JonH


2 Answers

Currently, AutoFixture supports the following validation attributes:

  • RangeAttribute
  • RegularExpressionAttribute
  • StringLengthAttribute

To support a custom validation attribute you'll have to extrapolate from one of the following groups:

  • RangeAttributeRelay, RangedNumberRequest, RangedNumberGenerator
  • RegularExpressionAttributeRelay, RegularExpressionRequest, RegularExpressionGenerator
  • StringLengthAttributeRelay, ConstrainedStringRequest, ConstrainedStringGenerator

And then, you'll have to add the newly created Relay and Generator classes as Customizations.

like image 79
Nikos Baxevanis Avatar answered Sep 30 '22 18:09

Nikos Baxevanis


I didn't want to bother creating a Relay, Request and Generator for each custom attribute I had in my solution. Instead, I create an ISpecimenBuilder that handled everything in one place.

In my case, I only have custom attributes on strings. Some custom attributes I created that I was looking for were AlphaNumericAttribute, and EnumAttribute.

So something like this:

public class CustomAnnotationsBuilder : ISpecimenBuilder
{
    private readonly Random rnd = new Random();

    public object Create(object request, ISpecimenContext context)
    {
        object result = new NoSpecimen(request);

        var pi = request as PropertyInfo;

        // do a few different inspections if it's a string
        if (pi != null && pi.PropertyType == typeof(string))
        {                
            // handle Enum attributes
            if (Attribute.IsDefined(pi, typeof(EnumAttribute)))
            {
                var enumAttribute = (EnumAttribute)Attribute.GetCustomAttribute(
                    pi, typeof(EnumAttribute));
                var allowedStrings = enumAttribute.GetAllowedStrings();
                return allowedStrings[rnd.Next(0, allowedStrings.Length)];
            }                
          
            if (Attribute.IsDefined(pi, typeof(StringLengthAttribute)))
            {
                var stringLengthAttribute = (StringLengthAttribute)Attribute.GetCustomAttribute(
                    pi, typeof(StringLengthAttribute));
                minLength = stringLengthAttribute.MinimumLength;
                maxLength = stringLengthAttribute.MaximumLength;

                // do custom string generation here
                return generatedString;
            }

            if (Attribute.IsDefined(pi, typeof(AlphaNumericAttribute)))
            {
                // do custom string generation here
                return generatedString;
            }

            return result;
        }

        return result;
    }

Then I could add that to AutoFixture like so:

Fixture.Customizations.Add(new CustomAnnotationsBuilder());
Fixture.Customize(new NoDataAnnotationsCustomization()); // ignore data annotations since I'm handling them myself

And that was it!

like image 27
JonH Avatar answered Sep 30 '22 19:09

JonH