Search code examples
c#asp.net-coremoqxunit.netautofixture

Unit testing a controller throwing exception


I have a controller with the following signature:

public CustomerTypeController(
    IHttpContextAccessor accessor,
    IPrincipalProvider provider,
    IMapper mapper, 
    ILogger<CustomerTypeController> logger,
    ICustomerTypeService customerTypeService)
{ }

For now my Theory looks like this:

[Theory, AutoMoqData]
public void GetWhenHasCustomerTypesShouldReturnOneCustomerType(
    IFixture fixture,
    [Frozen] Mock<ICustomerTypeService> service,
    CustomerTypeController sut)
{
    //Arrange
    var items = fixture.CreateMany<Model.CustomerType>(3).ToList();

    //Act
    var result = sut.Get(1);

    //Assert
    Assert.IsType<OkResult>(result);
}

When I run this test as-is, I get the following exception:

AutoFixture.ObjectCreationExceptionWithPath : AutoFixture was unable to create an instance from Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo because creation unexpectedly failed with exception. Please refer to the inner exception to investigate the root cause of the failure.

Inner exception messages: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. System.ArgumentException: The type 'System.Object' must implement 'Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder' to be used as a model binder. (Parameter 'value')

What am I doing wrong, and how do I solve the problem?


Solution

  • TL;DR

    You should decorate the controller parameter in your test method with the [NoAutoProperties] attribute.

    [Theory, AutoMoqData]
    public void GetWhenHasCustomerTypesShouldReturnOneCustomerType(
        IFixture fixture,
        [Frozen] Mock<ICustomerTypeService> service,
        [NoAutoProperties] CustomerTypeController sut)
    {
        //Arrange
        var items = fixture.CreateMany<Model.CustomerType>(3).ToList();
    
        //Act
        var result = sut.Get(1);
    
        //Assert
        Assert.IsType<OkResult>(result);
    }
    

    Update

    Now that I know the AutoFixture code-base a little better, I wanted to understand why does this actually fix the issue.

    The Greedy attribute normally instructs AutoFixture to use the constructor with the largest number of parameters, which should have nothing to do with the fix.

    As the error message states, the exception occurs when a property is being set and the property is expecting a value that implements IModelBinder. The origin of the error is the BinderType property of the BindingInfo class, which is of type System.Type. By default AutoFixture will resolve Type as System.Object, which explains the error message.

    When the Greedy attribute is applied, this customizes AutoFixture to create an instance of the property type, using a custom factory. The resulting builder graph node, (likely by accident) skips setting any properties, on the created instance.

    Taking this into consideration, a more fitting resolution should be a using the NoAutoProperties attribute. This will explicitly instruct AutoFixture to ignore all auto-properties in the decorated type, but will leave the constructor query as "modest".

    Since adding the attribute everywhere might get annoying and tedious, I suggest customizing AutoFixture to ignore all properties from ControllerBase, in the domain customization. Also in case you're using property injection, this will allow AutoFixture to instantiate the controller properties.

    public class AutoMoqDataAttribute : AutoDataAttribute
    {
        public AutoMoqDataAttribute()
            : base(() => new Fixture().Customize(
                new CompositeCustomization(
                    new AutoMoqCustomization(),
                    new AspNetCustomization())))
        {
        }
    }
    
    public class AspNetCustomization : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            fixture.Customizations.Add(new ControllerBasePropertyOmitter());
        }
    }
    
    public class ControllerBasePropertyOmitter : Omitter
    {
        public ControllerBasePropertyOmitter()
            : base(new OrRequestSpecification(GetPropertySpecifications()))
        {
        }
    
        private static IEnumerable<IRequestSpecification> GetPropertySpecifications()
        {
            return typeof(ControllerBase).GetProperties().Where(x => x.CanWrite)
                .Select(x => new PropertySpecification(x.PropertyType, x.Name));
        }
    }
    

    If you need the properties in ControllerBase for some reason then just instruct AutoFixture how to properly create BindingInfo instances.


    Original answer

    You should decorate the controller parameter in your test method with the [Greedy] attribute.

    [Theory, AutoMoqData]
    public void GetWhenHasCustomerTypesShouldReturnOneCustomerType(
        IFixture fixture,
        [Frozen] Mock<ICustomerTypeService> service,
        [Greedy] CustomerTypeController sut)
    {
        //Arrange
        var items = fixture.CreateMany<Model.CustomerType>(3).ToList();
    
        //Act
        var result = sut.Get(1);
    
        //Assert
        Assert.IsType<OkResult>(result);
    }