Why does Autofixture w/ AutoMoqCustomization stop

2019-02-12 20:35发布

问题:

When I use Moq directly to mock IBuilderFactory and instantiate BuilderService myself in a unit test, I can get a passing test which verifies that the Create() method of IBuilderFactory is called exactly once.

However, when I use Autofixture with AutoMoqCustomization, freezing a mock of IBuilderFactory and instantiating BuilderService with fixture.Create<BuilderService>, I get the following exception:

System.ArgumentException: Can not instantiate proxy of class: OddBehaviorTests.CubeBuilder. Could not find a parameterless constructor. Parameter name: constructorArguments

If I make CubeBuilder sealed (represented by replacing it with the sealed class SealedCubeBuilder which is called by IBuilderFactoryForSealedBuilder.Create(), the test passes using AutoFixture with AutoMoqCustomization, with no exception thrown.

Am I missing something? Since I get passing tests using Moq directly, I believe this is related to Autofixture and/or AutoMoqCustomization. Is this the desired behavior? If so, why?

To reproduce, I'm using:

using Moq;
using Ploeh.AutoFixture;
using Ploeh.AutoFixture.AutoMoq;
using Xunit;

Here are the four tests illustrating the behavior:

public class BuilderServiceTests {
    [Fact]
    public void CubeBuilderFactoryCreateMethodShouldBeCalled_UsingMoq() {
        var factory = new Mock<IBuilderFactory>();
        var sut = new BuilderService(factory.Object);
        sut.Create();
        factory.Verify(f => f.Create(), Times.Once());
    }
    [Fact]
    public void CubeBuilderFactoryCreateMethodShouldBeCalled_UsingAutoFixture() {
        var fixture = new Fixture().Customize(new AutoMoqCustomization());
        var factory = fixture.Freeze<Mock<IBuilderFactory>>();
        var sut = fixture.Create<BuilderService>();
        sut.Create(); // EXCEPTION THROWN!!
        factory.Verify(f => f.Create(), Times.Once());
    }
    [Fact]
    public void SealedCubeBuilderFactoryCreateMethodShouldBeCalled_UsingMoq() {
        var factory = new Mock<IBuilderFactoryForSealedBuilder>();
        var sut = new BuilderServiceForSealedBuilder(factory.Object);
        sut.Create();
        factory.Verify(f => f.Create(), Times.Once());
    }
    [Fact]
    public void SealedCubeBuilderFactoryCreateMethodShouldBeCalled_UsingAutoFixture() {
        var fixture = new Fixture().Customize(new AutoMoqCustomization());
        var factory = fixture.Freeze<Mock<IBuilderFactoryForSealedBuilder>>();
        var sut = fixture.Create<BuilderServiceForSealedBuilder>();
        sut.Create();
        factory.Verify(f => f.Create(), Times.Once());
    }
}

Here are the required classes:

public interface IBuilderService { IBuilder Create(); }
public class BuilderService : IBuilderService {
    private readonly IBuilderFactory _factory;
    public BuilderService(IBuilderFactory factory) { _factory = factory; }
    public IBuilder Create() { return _factory.Create(); }
}
public class BuilderServiceForSealedBuilder : IBuilderService {
    private readonly IBuilderFactoryForSealedBuilder _factory;
    public BuilderServiceForSealedBuilder(IBuilderFactoryForSealedBuilder factory) { _factory = factory; }
    public IBuilder Create() { return _factory.Create(); }
}

public interface IBuilderFactoryForSealedBuilder { SealedCubeBuilder Create(); }
public interface IBuilderFactory { CubeBuilder Create(); }
public interface IBuilder { void Build(); }

public abstract class Builder : IBuilder {
    public void Build() { } // build stuff 
}

public class CubeBuilder : Builder {
    private Cube _cube;
    public CubeBuilder(Cube cube) { _cube = cube; }
}

public sealed class SealedCubeBuilder : Builder {
    private Cube _cube;
    public SealedCubeBuilder(Cube cube) { _cube = cube; }
}

public class Cube { }

回答1:

If you look at the stack trace, you'll notice that the exception happens deep within Moq. AutoFixture is an opinionated library, and one of the opinions it holds is that nulls are invalid return values. For that reason, AutoMoqCustomization configures all Mock instances like this:

mock.DefaultValue = DefaultValue.Mock;

(among other things). Thus, you can reproduce the failing test entirely without AutoFixture:

[Fact]
public void ReproWithoutAutoFixture()
{
    var factory = new Mock<IBuilderFactory>();
    factory.DefaultValue = DefaultValue.Mock;
    var sut = new BuilderService(factory.Object);
    sut.Create(); // EXCEPTION THROWN!!
    factory.Verify(f => f.Create(), Times.Once());
}

The strange thing is that it still seems to work with sealed classes. This is, however, not quite true, but rather originates in the OP tests being False Negatives.

Consider this Characterization Test of Moq:

[Fact]
public void MoqCharacterizationForUnsealedClass()
{
    var factory = new Mock<IBuilderFactory>();
    factory.DefaultValue = DefaultValue.Mock;
    Assert.Throws<ArgumentException>(() => factory.Object.Create());
}

Moq correctly throws an exception because it's been asked to create an instance of CubeBuilder, and it doesn't know how to do that, because CubeBuilder has no default constructor, and no Setup tells it how to deal with calls to Create.

(As an aside, the irony here is that AutoFixture would be perfectly able to create an instance of CubeBuilder, but there's no extensibility point in Moq that enables AutoFixture to go in and take over Moq's default object instance creation behaviour.)

Now consider this Characterization test when a return type is sealed:

[Fact]
public void MoqCharacterizationForSealedClass()
{
    var factory = new Mock<IBuilderFactoryForSealedBuilder>();
    factory.DefaultValue = DefaultValue.Mock;
    var actual = factory.Object.Create();
    Assert.Null(actual);
}

It turns out that in this case, despite having been implicitly told not to return null, Moq does so anyway.

My theory is that what's really going on is that in MoqCharacterizationForUnsealedClass above, what factory.DefaultValue = DefaultValue.Mock; really means is that Moq creates a mock of CubeBuilder - in other words, it dynamically emits a class that derives from CubeBuilder. However, when asked to create a mock of SealedCubeBuilder, it can't, because it can't create a class derived from a sealed class.

Instead of throwing an exception, it returns null. This is inconsistent behaviour, and I've reported this as a bug in Moq.