Applying DRY to Autofixture "Build" statements
Asked Answered
S

1

13

Assume I have this concrete class:

public partial class User
{
    public int ID { get; set; }
    public string Email { get; set; }
    public string FullName { get; set; }
}

And I want to create an anonymous instance that has a valid email address, and the fullname field is no more than 20 characters. I can do this:

var fixture = new Fixture();
var anonUser = fixture.Build<User>()
    .With(x => x.Email, string.Format("{0}@fobar.com", fixture.Create<string>()))
    .With(x => x.FullName,  fixture.Create<string>()Substring(0,20))
    .Create();

Is there a way that I can define this in one place, so that AF knows that I can get my customized anon class by using:

var newAnon = fixture.Build<User>();
Steamy answered 11/3, 2014 at 17:11 Comment(1)
I strongly recommend using convention-based customizations as @MarkSeemann suggests in his answer to keep your test code DRY. More on the topic here.Gyasi
D
12

You have various options. In my opinion, the best option is to apply the GOOS principle of listening to your tests. When the test becomes difficult to write, it's time to reconsider the design of the System Under Test (SUT). AutoFixture tends to amplify this effect.

Refactor to Value Objects

If you have a requirement that the Email and FullName properties should have particularly constrained values, it might indicate that instead of Primitive Obsession, the target API would benefit from defining explicit Email and FullName Value Objects. The canonical AutoFixture example is about phone numbers.

Use data annotations

You can also use data annotations to give AutoFixture hints about the constraints of the values. Not all data annotation attributes are supported, but you can use both MaxLength and RegularExpression.

It might look something like this:

public partial class User
{
    public int ID { get; set; }
    [RegularExpression("regex for emails is much harder than you think")]
    public string Email { get; set; }
    [MaxLenght(20)]
    public string FullName { get; set; }
}

Personally, I don't like this approach, because I prefer proper encapsulation instead.

Use Customize

Instead of using the Build<T> method, use the Customize<T> method:

var fixture = new Fixture();
fixture.Customize<User>(c => c
    .With(x => x.Email, string.Format("{0}@fobar.com", fixture.Create<string>())
    .With(x => x.FullName, fixture.Create<string>().Substring(0,20)));
var newAnon = fixture.Create<User>();

Write a convention-driven Specimen Builder

Finally, you can write a convention-driven customization:

public class EmailSpecimenBuilder : ISpecimenBuilder
{
    public object Create(object request,
        ISpecimenContext context)
    {
        var pi = request as PropertyInfo;
        if (pi == null)
        {
            return new NoSpecimen(request);
        }

        if (pi.PropertyType != typeof(string)
            || pi.Name != "Email")
        {
            return new NoSpecimen(request);
        }

        return string.Format("{0}@fobar.com", context.Resolve(typeof(string)));
    }
}

This approach I really like, because I can put arbitrarily complex logic here, so instead of having to create a lot of one-off customizations, I can have a small set of conventions driving an entire test suite. This also tends to make the target code more consistent.

Drape answered 11/3, 2014 at 18:26 Comment(2)
ISpecimenBuilder seems to do the trick. What is the difference between Customize and Build? Did you try the above code for Customize? The compiler doesn't seem to grok it.Steamy
Build is a one-off specialization of the creation algorithm. Customize is reusable. No, I actually didn't try to compile the above code, so I may have missed a parenthesis or similar...Drape

© 2022 - 2024 — McMap. All rights reserved.