Why does FakeItEasy throw this exception, and why does making the method virtual fix it?
Asked Answered
P

3

14

I have a test (code is below) to test that Method1 calls Method2. The exception I'm getting is

The current proxy generator can not intercept the specified method for the following reason: - Sealed methods can not be intercepted.

The method under test isn't sealed itself. However, it does have a dependency on a sealed class (a third-party class for which I am having trouble creating a wrapper in order to mock it properly - another topic for another question). Either way, at this point I'm not asking FakeItEasy to mock the sealed class. And while debugging my test, when the dependency is called, I can clearly see that a real object is being produced, not a fake.

And yet, given the error message, I feel like it might be related somehow.

Further, I discovered through a random blog post that making the method virtual fixes the problem, allowing the test to pass. I gave it a try and it worked. But I don't get why it fixed it, and regardless, it doesn't make sense for me to keep the method virtual. In my case, the class being tested doesn't have any children of its own, ie; no children to override its methods, so I can't see any reason to make it virtual.

Am I wrong in thinking I have no reason to make the method virtual?
Is FakeItEasy somehow trying to mock that sealed class?

I'm really not sure how to proceed with this test.

My Test

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


Related Methods

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
    {            
    }
}
Purport answered 11/7, 2014 at 16:41 Comment(0)
A
25

While normally applied to the class scope, sealed in this case refers to the inability to override the method in question. Using sealed with a method is only valid if the method is an override - however, methods which are not virtual in the first place cannot be overidden, and are thus themselves implicitly sealed.

What this error refers to is that it cannot accept non-virtual methods, due to the fact that it is creating a class on the fly inherited from your given class to perform these intercepts. At such a level, it can neither determine the difference between a non-vritual and sealed method, nor does it need to - it cannot override, and thus cannot insert the appropriate intercept.

Arp answered 11/7, 2014 at 16:46 Comment(4)
Great answer. One clarification, though: sealed is valid on methods. It has the effect of cancelling any virtual that may have been applied on the parent method. And as you say, FakeItEasy uses the Castle Dynamic Proxy to create a class that inherits from Service2. When A.CallTo is executed, it finds that the method can not be overridden, so FakeItEasy can't intercept the calls.Defiance
well, looks like I have to explore that a bit more. Thanks for the clairifcation, I'll integrate that!Arp
Thank you - I actually didn't realize that FakeItEasy was creating classes on the fly like this. It sounds like I need to read up on how it works behind the scenes. Hopefully I can find some explanations that are clear enough for my experience level. I am particularly curious about why I haven't ran into this issue before - since none of the other methods I've tested are virtual either - and what it is about Service2 that is different. I've had similar setups testing MVC action methods - injecting the controller w/a service class, and verifying a call was made to a method in the service class.Purport
Considering it further, I think the reason I didn't see this error before was because I happened to be injecting interfaces in my other tests, as described in the other answer.Purport
D
11

Aravol's answer is great. I urge you to accept it and/or vote it up.

There's another approach, though. Make _service2 a fake IService2.

Then you don't have to change the signature of any methods (interface methods are always overridable/interceptable). In general, it's easier to fake interface interfaces than concrete (or even abstract) classes, and it has the nice effect of actually testing that your collaborating classes can work with interfaces, not necessarily just with particular implementations of the interfaces.

While I'm here, and this part isn't really related to your error, but may help make your code a little clearer, since you're testing Service1, I would not fake it; use an actual Service1 instance. This makes it clear to readers what's actually being tested. Faking the system under test is widely considered to be a code smell.

If you happened to take both these points, your SetUp would look a little more like this:

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

And your test should pass.

Defiance answered 11/7, 2014 at 17:54 Comment(1)
Thank you - I appreciate not having to make my method virtual just for the sake of my test. And thanks for the suggestion about not faking Service1 - I am often unclear about what should/shouldn't be mocked.Purport
L
0

I've just been chasing a nearly identical problem for two days now. Blair Conrad's solution above of faking at the interface level worked for me, and also actually makes sense:

If the class being tested doesn't have a dependency on another class, neither should the test. Therefore you fake the interface, rather than a class that implements the interface.

Lippi answered 29/9, 2015 at 13:56 Comment(1)
I guess this should rather be a comment on cited answer then an answer itself as it doesn't provide a solution.Unmeant

© 2022 - 2024 — McMap. All rights reserved.