单元测试控制器抛出异常

Unit testing a controller throwing exception

我有一个具有以下签名的控制器:

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

现在我的 Theory 看起来像这样:

[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);
}

当我 运行 按原样进行此测试时,出现以下异常:

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')

我做错了什么,我该如何解决这个问题?

TL;DR

您应该使用 [NoAutoProperties] 属性修饰测试方法中的控制器参数。

[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);
}

更新

现在我对 AutoFixture 代码库有了更多的了解,我想了解为什么这实际上可以解决问题。

Greedy属性一般是指示AutoFixture使用参数个数最多的构造函数,应该与修复无关

如错误消息所述,当设置 属性 并且 属性 期望实现 IModelBinder 的值时会发生异常。错误的来源是 BindingInfo class 的 BinderType 属性,类型为 System.Type。默认情况下,AutoFixture 会将 Type 解析为 System.Object,这解释了错误消息。

当应用 Greedy 属性时,这会自定义 AutoFixture 以使用自定义工厂创建 属性 类型的实例。生成的构建器图节点(可能是偶然的)跳过了在创建的实例上设置任何属性。

考虑到这一点,更合适的解决方案应该是使用 NoAutoProperties 属性。这将明确指示 AutoFixture 忽略装饰类型中的所有自动属性,但会将构造函数查询保留为“适度”。

由于在任何地方添加属性可能会变得烦人和乏味,我建议在域自定义中自定义 AutoFixture 以忽略来自 ControllerBase 的所有属性。此外,如果您使用 属性 注入,这将允许 AutoFixture 实例化控制器属性。

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));
    }
}

如果您出于某种原因需要 ControllerBase 中的属性,那么只需指导 AutoFixture 如何正确创建 BindingInfo 个实例。


原回答

您应该使用 [Greedy] 属性修饰测试方法中的控制器参数。

[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);
}