soenneker / soenneker.utils.autobogus

The .NET Autogenerator
https://soenneker.com
MIT License
33 stars 4 forks source link

AutoFixture Extensability #263

Open jacob7395 opened 2 weeks ago

jacob7395 commented 2 weeks ago

Hey,

I have some thought about how to enhancement AutoFixture this is probably two issues but would like to get your opinion in one place befog creating some sub issues;

  1. Support integration with substitution libraries like NSubstitute
  2. Provide a generic way to allow to configure (set rules) for how AutoFaker generates specific types

Substitution Integration

Adding integration with substitution libraries shouldn't be too bad, the issue is many a lot of the components of AutoFixture are internal, or if not internal not using virtual to allow for extension f the required method. An option would be allowing the binder to be extended and making AutoFakerBinder.CreateInstance virtual. This would allow us to provide a specific binder that support the substitution library. Using the patten followed by the original library where the substitution bindings are provided as separate nuget packages to prevent unnecessary dependencies.

In addition to this a simple refactor to AutoFixture to use the interface IAutoFakerBinder (would require some extra methods to be pulled into it), could allow for more extendability. I will say that both changes option open up the door for people shooting themselves in the foot, but I don't see that as a reason to gatekeep the internals. This is a test library after all its hard to predict how people will want to use it so giving them the option to fiddle is my preference.

Fluent Type Building

While it is possible to configure specific types using the current AutoFaker and AutoFakerOverride, this approach is cumbersome and not intuitive. For example:

public record ChildModelDto(string StringValue);
public record BaseModelDto(string StringValue, ChildModelDto ChildModel);

public class BaseModelDtoFaker : AutoFaker<BaseModelDto>
{
    public BaseModelDtoFaker(AutoFakerConfig? autoFakerConfig = null) : base(autoFakerConfig)
    {
        RuleFor(d => d.StringValue, "BASEVALUE");
    }
}

public class BaseModelDtoFakerOverride(BaseModelDtoFaker faker) : AutoFakerOverride<BaseModelDto>
{
    public override bool Preinitialize => false;

    public override void Generate(AutoFakerOverrideContext context)
    {
        var rulesets = context.RuleSets is { } ? string.Join(',', context.RuleSets) : null;

        context.Instance = faker.Generate(rulesets);
    }
}

public class ChildModelDtoFaker : AutoFaker<ChildModelDto>
{
    public ChildModelDtoFaker(AutoFakerConfig? autoFakerConfig = null) : base(autoFakerConfig)
    {
        RuleFor(d => d.StringValue, "CHILDMODEL");
    }
}

public class ChildModelDtoOverride(ChildModelDtoFaker faker) : AutoFakerOverride<ChildModelDto>
{
    public override bool Preinitialize => false;

    public override void Generate(AutoFakerOverrideContext context)
    {
        var rulesets = context.RuleSets is { } ? string.Join(',', context.RuleSets) : null;

        context.Instance = faker.Generate(rulesets);
    }
}

public class ComplexModelGeneration
{
    [Fact]
    public void ConfigurePropertyModels()
    {
        var sharedConfig = new AutoFakerConfig()
        {
            Overrides = []
        };

        BaseModelDtoFaker baseFaker = new BaseModelDtoFaker(sharedConfig);
        sharedConfig.Overrides.Add(new BaseModelDtoFakerOverride(baseFaker));

        ChildModelDtoFaker childFaker = new ChildModelDtoFaker(sharedConfig);
        sharedConfig.Overrides.Add(new ChildModelDtoOverride(childFaker));

        var faker = new AutoFaker(sharedConfig);

        var b = faker.Generate<BaseModelDto>();
        var c1 = faker.Generate<ChildModelDto>();

        b.StringValue.Should().Be("BASEVALUE");
        b.ChildModel.StringValue.Should().Be("CHILDMODEL");

        c1.Should().BeEquivalentTo(b.ChildModel);
        c1.Should().NotBeSameAs(b.ChildModel);
    }
}

However, I don't believe the library was designed with this specific usage in mind. Therefore, I think it would be useful to provide a dedicated API to support this natively. I suggest introducing a fluent builder pattern that looks something like:

var faker = new AutoFaker(sharedConfig);

faker.RuleBuilder
    .RulesForType<BaseModelDto>(r => {
        r.RuleFor(b => b.StringValue, "BASEMODEL");
    })
    .RulesForType<ChildModelDto>(c => {
        c.RuleFor(d => d.StringValue, "CHILDMODEL")
    });

In the above I would expect the generation of BaseModelDto to use the rules defined in ChildModelDto. I don't think this should be supports out the gate but I wouldn't want to rule out something like this:

var faker = new AutoFaker(sharedConfig);

faker.RuleBuilder
    .RulesForType<BaseModelDto>(r => { //Where r would be a AutoFaker<BaseModelDto> or an interface that we can map to that type
        r.RuleFor(b => b.StringValue, "BASEMODEL");
        r.RulesForType<ChildModelDto>(c => {
            c.RuleFor(d => d.StringValue, "OVERRIDE")
        })
    })
    .RulesForType<ChildModelDto>(c => {
        c.RuleFor(d => d.StringValue, "CHILDMODEL")
    });

So the above could generate "CHILDMODEL" when just requesting a ChildModelDto but "OVERRIDE" when part of BaseModelDto. I think the override within the RulesForType<BaseModelDto> should be prioritized over the 'base' defection, maybe a priority like:

  1. Rules defined in child IFluentRuleTypeBuilder; RulesForType.RulesForType.RuleFor(d => d.StringValue
  2. Rules defined in base IFluentRuleTypeBuilder; RulesForType.RuleFor(d => d.StringValue
  3. Defined AutoFaker; these could be passed as to the RulesForType method
  4. Auto generated values;
interface IFluentRuleTypeBuilderBase
{
    public IFluentRuleTypeBuilder<T> RulesForType<T>(AutoFaker<T> baseFaker) where T : class;

    public IFluentRuleTypeBuilder<T> RulesForType<T>(AutoFaker<T> baseFaker, Func<IFluentRuleTypeBuilder<T>> configuration) where T : class;

    public IFluentRuleTypeBuilder<T> RulesForType<T>(Func<IFluentRuleTypeBuilder<T>> configuration);
}

interface IFluentRuleTypeBuilder<T> : IFluentRuleTypeBuilderBase
{
    // Implement the same interface as Faker
}

interface IFluentRuleBuilder : IFluentRuleTypeBuilderBase
{

}

If we open up AutoFaker in line with my first suggestions we could provide a separate package AutoFaker.FluentBuilder, that provide a new AutoFaker with this API specifically in mind. This would reduce the risk of breaking existing code, but may force workarounds or extensive overrides.


I would love to discuses both of these with you, how do you see the library being used? are these features you have though about supporting?

soenneker commented 2 weeks ago

Yes, I've known that the substitution libs have been needing to be done for a while... I should get on that soon. I can't remember completely, but I feel like there was an issue with simply making CreateInstance virtual because of underlying caching and some of the complexities with downstream manipulation.

I like the concept you have here with fluent rule generation. I'm not opposed to introducing something similar to what you have. I try my best to compose rather than inherit, thus I haven't had a huge need for complex rules personally.