Search code examples
c#autofixture

Creating public types in AutoFixture 4.8.0 using internal JSON constructors, with many constructor arguments


As at AutoFixture 4.8.0, is there a better alternative to using Fixture.Register to register a function to create a type that exposes only an internal constructor?

In context:

I'm working on a .net standard class library to build API for internal use. We do not want to expose public constructors and as such all constructors for these models are internal. Properties are additionally public readonly. These classes will be instantiated through Json deserialization, and in this particular library there is a model with > 20 properties to consider. As a bonus, we'd like to use [AutoDataAttribute] where possible.

ie. is there an alternative to supplying an internal jsonconstructor with a mixture of non and complex types (also internal) with 20+ arguments using AutoFixture?

[TestFixture]
public sealed class Tests
{
    private Fixture _fixture;

    [OneTimeSetUp]
    public void OneTimeSetup()
    {
        _fixture = new Fixture();

        _fixture.Register(() => new Vehicle(_fixture.Create<string>(), _fixture.Create<int>()));

        _fixture.Register(
            () => new Driver(_fixture.Create<string>(), _fixture.Create<int>(), _fixture.Create<Vehicle>()));
    }

    [Test]
    public void CanCreateDriverWithVehicle()
    {
        Func<Driver> func = () => _fixture.Create<Driver>();
        func.Should().NotThrow(); // Green
    }
}

[PublicAPI]
public sealed class Driver
{
    public readonly string Name;
    public readonly int Age;
    public Vehicle Vehicle;

    [JsonConstructor]
    internal Driver(string name, int age, Vehicle vehicle)
    {
        Name = name;
        Age = age;
        Vehicle = vehicle;
    }
}

[PublicAPI]
public sealed class Vehicle
{
    public readonly string Manufacturer;
    public readonly int Miles;

    [JsonConstructor]
    internal Vehicle(string manufacturer, int miles)
    {
        Manufacturer = manufacturer;
        Miles = miles;
    }
}

Solution

  • By default AutoFixture considers the public constructors only. It doesn't pick up the internal constructors, even if they are accessible from the test execution assembly (not even sure whether there is a reasonable technical possibility to check that).

    You can easily customize your fixture to support that. Get inspiration from the following code sample:

    [Fact]
    public void ShouldActivateTypesWithInternalConstructor()
    {
        var fixture = new Fixture();
    
        fixture.ResidueCollectors.Add(
            new Postprocessor(
                new MethodInvoker(
                    new ModestInternalConstructorQuery()),
                new AutoPropertiesCommand()
            ));
    
        var result = fixture.Create<TypeWithInternalConstructor>();
    
        Assert.NotEqual(0, result.Property);
    }
    
    public class ModestInternalConstructorQuery : IMethodQuery
    {
        public IEnumerable<IMethod> SelectMethods(Type type)
        {
            if (type == null) throw new ArgumentNullException(nameof(type));
    
            return from ci in type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)
                where ci.IsAssembly // Take internal constructors only
                let parameters = ci.GetParameters()
                where parameters.All(p => p.ParameterType != type)
                orderby parameters.Length ascending
                select new ConstructorMethod(ci) as IMethod;
        }
    }
    
    public class TypeWithInternalConstructor
    {
        public int Property { get; }
    
        internal TypeWithInternalConstructor(int property)
        {
            Property = property;
        }
    }