为什么 Matching.ImplementedInterfaces 的行为不同于 Matching.ExactType 和 FrozenAttribute.As?

Why Does the Behavior of Matching.ImplementedInterfaces Differ from Matching.ExactType and FrozenAttribute.As?

请考虑以下代码:

public class TestingSample
{
    public class FactoryClass : Class {}

    public class Class : IInterface {}

    public interface IInterface {}

    public class AutoData : AutoDataAttribute
    {
        public AutoData() : base( Create() ) {}

        static IFixture Create()
        {
            var fixture = new Fixture();
            fixture.Customize<IInterface>( composer => composer.FromFactory( () => new FactoryClass() ) );
            fixture.Customize<Class>( composer => composer.FromFactory( () => new FactoryClass() ) );
            return fixture;
        }
    }

    [Theory, TestingSample.AutoData]
    public void OldSkool( [Frozen( As = typeof(IInterface) )]Class first, Class second, IInterface third )
    {
        Assert.IsType<FactoryClass>( first );
        Assert.Same( first, second );
        Assert.Same( first, third );
    }

    [Theory, TestingSample.AutoData]
    public void DirectBaseType( [Frozen( Matching.ExactType )]Class first, Class second )
    {
        Assert.IsType<FactoryClass>( first );
        Assert.Same( first, second );
    }

    [Theory, TestingSample.AutoData]
    public void ImplementedInterfaces( [Frozen( Matching.ImplementedInterfaces )]Class first, IInterface second )
    {
        Assert.IsType<FactoryClass>( first );
        Assert.Same( first, second ); // The Fails.
    }
}

如您(希望)所见,ImplementedInterfaces 测试失败。由于 FrozenAttribute.As 已被弃用并且用户已被引导到 Match 枚举,我的期望是它的行为与以前相同。

但是,Match.ImplementedInterfaces 的行为似乎与 Match.ExactTypeFrozenAttribute.As 不同。

我确实做了一些探索,发现 Match.ExactTypeFrozenAttribute.As 使用 SeedRequestSpecificationMatch.ImplementedInterfaces 只匹配 Type 请求。

是否可以获得有关此行为的一些背景信息?这是设计使然吗?如果是这样,是否有已知的建议以这种方式进行设计以使用 Match.ImplementedInterfaces 恢复旧行为?

首先,一个附带条件:在我的机器上,使用 AutoFixture 3.39.0,OP 中提供的代码并不像描述的那样相当。不同的是,这个测试中的第一个断言通过了:

[Theory, TestingSample.AutoData]
public void ImplementedInterfaces(
    [Frozen(Matching.ImplementedInterfaces)]Class first,
    IInterface second)
{
    Assert.IsType<FactoryClass>(first); // passes
    Assert.Same(first, second); // fails
}

不过,我承认第二个断言失败(有点)令人惊讶。

简短的解释是,在当前的实施中,冻结是在 reflection-time 上完成的,而不是在 run-time 上完成的。当 AutoFixture.Xunit2 确定要冻结的内容时,它会查看应用了 [Frozen] 属性的参数类型。这是 Class,而不是 FactoryClass,所以结果是 FactoryClass 根本没有被冻结!

你可以从这个测试中看出这一点:

[Theory, TestingSample.AutoData]
public void FactoryClassIsNotFrozen(
    [Frozen(Matching.ImplementedInterfaces)]Class first,
    FactoryClass second)
{
    Assert.IsType<FactoryClass>(first); // passes
    Assert.IsType<FactoryClass>(second); // passes
    Assert.Same(first, second); // fails
}

这是最好的实现方式吗?也许不是,但这就是它目前的工作方式。 an open issue in the AutoFixture GitHub repository 建议重构冻结实现,使其更像 DI 容器的单例生命周期。这可能会将此特定场景中的行为改变为更适合它的行为。它是否也会有一些缺点我现在还不能说。

当我们重新设计 [Frozen] 属性以使用更灵活的 Matching 规则时,我意识到新系统无法 100% 替代旧 As属性。我仍然认为 trade-off 值得。

虽然 As 使您能够使用此特定功能,但这是因为作为程序员,您 知道 Class 实现了 IInterface,因此 [Frozen(As = typeof(IInterface))] 注释是有意义的。

你可能会说 As 更灵活,但这主要是因为它没有 built-in 智能。您也可以编写 [Frozen(As = typeof(IAsyncResult))] 并且编译得很好 - 只是在 run-time 处失败,因为它完全是胡说八道。

is there a known recommendation to design in such a way to restore the old behavior using Match.ImplementedInterfaces?

是,考虑简化被测系统 (SUT) 的设计。

AutoFixture 最初被设想为 Test-Driven 开发工具,这仍然是它的主要目的。本着 GOOS 的精神,我们应该 听测试 。如果测试难写,第一反应应该是简化SUT。 AutoFixture 倾向于放大来自测试的此类反馈。

您真的需要匹配既实现接口又派生自基础的东西吗class?为什么?

能再简单点吗?