Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide() introduces a new scope, which causes behavior configured via A.CallTo() to fail to work as expected #22

Open
katiejanekilian opened this issue Jun 14, 2021 · 3 comments

Comments

@katiejanekilian
Copy link

The current behavior of the AutoFake class only works as expected (or at least, as I expected) if I make all calls to Provide<T>() before any calls to A.CallTo().

Provide<T>() and the other Provide methods introduce a new ILifetimeScope each time they are called. This appears to break any fakes configured via FakeItEasy's A.CallTo() via the method described in the Autofac.Extras.FakeItEasy documentation if they were configured before the call(s) to Provide.

Steps to Reproduce

This code demonstrates the problem.

public interface IStringService { string GetString(); }

public static void ACallTo_before_Provide()
{
    using (var fake = new AutoFake())
    {
        A.CallTo(() => fake.Resolve<IStringService>().GetString())
            .Returns("Test string");

        fake.Provide(new StringBuilder());
        
        var stringService = fake.Resolve<IStringService>();
        string result = stringService.GetString();

        // FAILS. The result should be "Test string",
        // but instead it's an empty string.
        Assert.Equal($"Test string", result);
    }
}

If you swap the order of the calls to A.CallTo() and Provide<T>() so Provide is called first, it works as expected.

Expected Behavior

I would expect the order I configure dependences not to matter in the unit tests. I think this is a bug? I opened a question on StackOverflow; @blairconrad replied, but wasn't sure if this is the correct behavior or not.

It seems to me it won't always be possible to call Provide() before I configure objects with A.CallTo(). I don't see anything in the documentation suggesting the two are incompatible, or anything saying I need to make sure all calls to Provide() come before any configuration via A.CallTo().

Dependency Versions

Autofac 6.2.0
Autofac.Extras.FakeItEasy 7.0.0
FakeItEasy 7.1.0

Additional Info

Here is a GitHub repository demonstrating the problem in more detail. It demonstrates that dependencies can be configured in any order if you solely use Provide(), or if you solely use A.CallTo(). Only if you mix the two does the order becomes important, furthering my suspicion that this is a bug.

Also, in case it helps, @blairconrad found that the stacked scope behavior causing this problem was introduced in PR 18. He wasn't sure why.

@tillig
Copy link
Member

tillig commented Jun 14, 2021

The additional scope stuff is there because the container is immutable - the only way to add new registrations "on the fly" is to register them during creation of a new lifetime scope.

Unfortunately, I'm not personally going to be able to allocate time to this. The FakeItEasy integration was provided by those folks and while we have it under the Autofac repo setup, none of the core maintainers of Autofac are FakeItEasy users. (I could be wrong, and if I am, maybe that individual can chime in here.)

If you have a fix, we'd be absolutely open to a pull request. However, again, I'm not sure that I'm going to be able to personally handle this, and if no other Autofac maintainers jump in, that means it'll remain open until a PR shows up.

@blairconrad
Copy link
Contributor

To clarify slightly:

This appears to break any fakes configured via FakeItEasy's A.CallTo() via the method described in the Autofac.Extras.FakeItEasy documentation if they were configured before the call(s) to Provide.

this statement is technically accurate, but I want to stress that that the Fakes' configuration isn't broken. It's just that a Resolve called after Provide returns a different Fake than the Resolve that's called before. The original Fake would still be configured the same way.

Carrying on, I have not debugged, but based on the observed behaviour, I expect that AutoFakeItEasy is looking in the new lifetime scope for a Fake of the right type, not finding it, and then creating one. From the lifetime documentation, which states

In general, a component will try to get its dependencies from the lifetime scope resolving the component. For example, if you’re in one of the child lifetime scopes and try to resolve something, Autofac will try to get all of the component’s dependencies from the child scope.
[…]

A child lifetime scope can get dependencies from parent scopes, but a parent scope may not reach down into a child scope. (You can locate things by moving “up the tree” but you can’t move “down the tree.”)

This suggests to me that it would be legal to return a Fake from a parent scope if none were defined in the current scope. (In fact, I'm not entirely sure why this isn't already happening. Is the scope-walking only something that's used when retrieving a component?) Maybe the existing code just needs a tweak to enable that behaviour

@jasonwil
Copy link

I am working around this by swapping the fakes' registrations from InstancePerLifetimeScope to SingleInstance, So far I haven't found any place in our codebase where we actually need two independent fake objects in separate scopes, but if I do run into it I think it will be more straightforward to deal with than the stock Provides behavior.

Perhaps this behavior could be configurable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants