为什么FakeItEasy会抛出此异常,将方法改为虚方法为什么能够修复它?

14

我有一个测试(代码如下)来测试Method1是否调用了Method2。 我得到的异常是:

当前代理生成器无法拦截指定的方法,原因是以下原因:- 密封方法无法拦截。

被测试的方法本身并不是sealed类型。 但是,它确实依赖于一个sealed类(一个第三方类,我很难创建包装器以正确模拟它 - 这是另一个问题的话题)。 无论如何,在这一点上,我没有要求FakeItEasy模拟密封类。 而在调试我的测试时,当依赖项被调用时,我可以清楚地看到一个真实的对象被生成,而不是假的。

然而,考虑到错误消息,我觉得它可能有些关联。

此外,我通过随机博客文章发现,将该方法设为虚方法可以解决问题,从而使测试通过。 我尝试了一下,它确实起作用了。 但我不明白为什么会这样,并且无论如何,保持该方法虚方法无意义。 在我的情况下,被测试的类没有自己的子类,即没有子类重写其方法,因此我看不到任何理由使它成为虚方法。

我错误地认为不必使该方法成为虚方法吗? FakeItEasy是否在尝试模拟密封类?

我真的不知道如何继续进行这个测试。

我的测试代码

[SetUp]
public void SetUp()
{
    // Arrange
    _service2 = A.Fake<Service2>(x => x.WithArgumentsForConstructor(
                                        () => new Service2()));
    _service1 = A.Fake<Service1>(x => x.WithArgumentsForConstructor(
                                        () => new Service1(_service2)));
}

[Test]
public void when_Method1_executes_it_calls_Method2()
{
    // Act
    result = _service1.Method1();

    // Assert
     A.CallTo(() => _service2.Method2())
                                   .WithAnyArguments()
                                   .MustHaveHappened();
}


相关方法

public class Service1 : IService1
{
    private readonly IService2 _service2;
    public Service1(IService2 service2)
    { 
        _service2 = service2; 
    }

    public bool Method1()
    {
        using (var dependency = new MyDependency()) // third party sealed class
        {
        }

        var x = _service2.Method2();            
    }
}   


public class Service2 : IService2
{
    public bool Method2() // making this virtual fixes the FakeItEasy exception
    {            
    }
}
3个回答

25

虽然通常应用于类范围,但在这种情况下,sealed指的是无法覆盖所讨论的方法。仅当方法是override时,使用sealed才是有效的 - 然而,首先不是虚拟的方法不能被覆盖,因此它们本身是隐式封闭的。

这个错误指的是它不能接受非virtual方法,因为它正在创建一个从给定类继承的临时类来执行这些拦截。在这个级别上,它既不能确定非虚拟和密封方法之间的区别,也不需要这样做 - 它不能覆盖,因此无法插入适当的拦截。


3
好的回答。需要澄清一点:sealed 可以应用于方法上,它会取消任何已应用在父方法上的虚拟属性。正如你所说,FakeItEasy使用了Castle Dynamic Proxy来创建一个继承自 Service2 的类。当执行 A.CallTo 时,它发现该方法无法被重写,因此FakeItEasy不能截取该调用。 - Blair Conrad
好的,看起来我需要更深入地探索一下。感谢您的澄清,我会将其整合进去! - David
谢谢 - 我实际上没有意识到FakeItEasy像这样动态创建类。听起来我需要了解它在幕后是如何工作的。希望我能找到一些对我的经验水平足够清晰的解释。我特别好奇为什么我以前没有遇到过这个问题 - 因为我测试过的其他方法都不是虚拟的 - 以及Service2有什么不同之处。我曾经进行过类似的设置来测试MVC操作方法 - 注入控制器w / a服务类,并验证是否调用了服务类中的方法。 - EF0
1
进一步考虑,我认为我之前没有看到这个错误的原因是因为我碰巧在我的其他测试中注入了接口,就像另一个答案中描述的那样。 - EF0

11

Aravol的回答很好。我建议您接受并/或投票支持。

不过,还有另一种方法。将_service2作为一个假的IService2

然后您就不必改变任何方法的签名(接口方法始终是可重写/拦截的)。总的来说,伪造接口比具体(甚至抽象)类更容易,并且它具有实际测试协作类能够使用接口而不一定只是特定接口实现的良好效果。

顺便提一下,这部分内容与您的错误实际上没有关系,但可能有助于使您的代码更清晰,因为您正在测试Service1,我不会伪造它;使用真正的Service1实例。这使读者清楚地知道实际上正在进行什么测试。被伪造的测试系统通常被认为是代码异味。

如果您采取了这两个建议,那么您的SetUp看起来会更像这样:

[SetUp]
public void SetUp()
{
    // Arrange
    _service2 = A.Fake<IService2>();
    _service1 = new Service1(_service2);
}

而且你的测试应该通过。


谢谢 - 我很感激不必为了测试而将我的方法设为虚拟的建议。还有感谢你关于不伪造Service1的建议 - 我经常不清楚什么应该/不应该被模拟。 - EF0

0
我已经为一个几乎相同的问题奔波了两天。Blair Conrad上面提供的在接口层面上伪造的解决方案对我有用,而且实际上也很有道理:
如果被测试的类没有依赖于另一个类,那么测试也不应该有依赖。因此,你要伪造接口,而不是实现接口的类。

2
我想这应该是对引用答案的评论,而不是一个答案,因为它并没有提供解决方案。 - m02ph3u5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接