SpecFlowOSS / SpecFlow

#1 .NET BDD Framework. SpecFlow automates your testing & works with your existing code. Find Bugs before they happen. Behavior Driven Development helps developers, testers, and business representatives to get a better understanding of their collaboration
https://www.specflow.org/
Other
2.23k stars 752 forks source link

'BoDi.ObjectContainerException: Interface cannot be resolved' when running features in parallel #2577

Closed shack05 closed 2 years ago

shack05 commented 2 years ago

SpecFlow Version

3.9.22

Which test runner are you using?

MSTest

Test Runner Version Number

2.2.7

.NET Implementation

.NET Core 3.1

Project Format of the SpecFlow project

Sdk-style project format

.feature.cs files are generated using

SpecFlow.Tools.MsBuild.Generation NuGet package

Test Execution Method

Visual Studio Test Explorer

SpecFlow Section in app.config or content of specflow.json

N/A

Issue Description

If I try to run features in parallel the following exception is thrown for the second scenario if a binding class has a dependency on an interface whose implementation is registered in the TestThreadContainer:

BoDi.ObjectContainerException: Interface cannot be resolved: ...

Full error:

Message: 
    Test method SpecFlowProject2.Features.CalculatorFeature.AddTwoNumbers threw exception: 
    BoDi.ObjectContainerException: Interface cannot be resolved: SpecFlowProject2.Steps.IFoo (resolution path: SpecFlowProject2.Steps.CalculatorStepDefinitions)
  Stack Trace: 
    <>c__DisplayClass3_0.<ResolvePerContext>b__1()
    RegistrationWithStrategy.ExecuteWithLock(Object lockObject, Func`1 getter, Func`1 factory, ResolutionList resolutionPath)
    TypeRegistration.ResolvePerContext(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    RegistrationWithStrategy.Resolve(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.ResolveObject(RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.Resolve(Type typeToResolve, ResolutionList resolutionPath, String name)
    <>c__DisplayClass71_0.<ResolveArguments>b__0(ParameterInfo p)
    SelectArrayIterator`2.ToArray()
    Enumerable.ToArray[TSource](IEnumerable`1 source)
    ObjectContainer.ResolveArguments(IEnumerable`1 parameters, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.CreateObject(Type type, ResolutionList resolutionPath, RegistrationKey keyToResolve)
    <>c__DisplayClass3_0.<ResolvePerContext>b__1()
    RegistrationWithStrategy.ExecuteWithLock(Object lockObject, Func`1 getter, Func`1 factory, ResolutionList resolutionPath)
    TypeRegistration.ResolvePerContext(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    RegistrationWithStrategy.Resolve(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.ResolveObject(RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.Resolve(Type typeToResolve, ResolutionList resolutionPath, String name)
    ObjectContainer.Resolve(Type typeToResolve, String name)
    TestObjectResolver.ResolveBindingInstance(Type bindingType, IObjectContainer container)
    lambda_method(Closure , IContextManager , Int32 )
    BindingInvoker.InvokeBinding(IBinding binding, IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration)
    TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments, TimeSpan& duration)
    TestExecutionEngine.ExecuteStep(IContextManager contextManager, StepInstance stepInstance)
    TestExecutionEngine.OnAfterLastStep()
    TestRunner.CollectScenarioErrors()
    CalculatorFeature.ScenarioCleanup()
    CalculatorFeature.AddTwoNumbers() line 13

Steps to Reproduce

Please see repro.

Link to Repro Project

repro. The registration hook, interface, and implementation are all in CalculatorStepDefinitions.

SabotageAndi commented 2 years ago

@gasparnagy Any ideas what is happening here?

shack05 commented 2 years ago

Are BeforeTestRun hooks supposed to run once for each thread? It seems that the registrations defined in the hook are only being run once and therefore the registrations are only being applied to the first test thread container?

Here's another example showing a similar exception. In this example I am registering an instance rather than a concrete type for an interface.

Example code:

public class Person
    {
        public Person(string name)
        {
            Name = name ?? throw new ArgumentNullException(nameof(name));
        }

        public string Name { get; }
    }

    [Binding]
    public sealed class RegistrationHooks
    {
        [BeforeTestRun]
        public static void RegisterTestThreadDependencies(TestThreadContext testThreadContext)
        {
            var person = new Person("Foo");
            testThreadContext.TestThreadContainer.RegisterInstanceAs(person);
        }
    }

    [Binding]
    public sealed class CalculatorStepDefinitions
    {
        private readonly Person _person;

        public CalculatorStepDefinitions(Person person)
        {
            _person = person;
        }

        ...
    }

Error:

Message: 
    Test method SpecFlowProject2.Features.CalculatorFeature.AddTwoNumbers threw exception: 
    BoDi.ObjectContainerException: Primitive types or structs cannot be resolved: System.String (resolution path: SpecFlowProject2.Steps.CalculatorStepDefinitions->SpecFlowProject2.Steps.Person)
  Stack Trace: 
    ObjectContainer.ResolveObject(RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.Resolve(Type typeToResolve, ResolutionList resolutionPath, String name)
    <>c__DisplayClass71_0.<ResolveArguments>b__0(ParameterInfo p)
    SelectArrayIterator`2.ToArray()
    Enumerable.ToArray[TSource](IEnumerable`1 source)
    ObjectContainer.ResolveArguments(IEnumerable`1 parameters, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.CreateObject(Type type, ResolutionList resolutionPath, RegistrationKey keyToResolve)
    <>c__DisplayClass3_0.<ResolvePerContext>b__1()
    RegistrationWithStrategy.ExecuteWithLock(Object lockObject, Func`1 getter, Func`1 factory, ResolutionList resolutionPath)
    TypeRegistration.ResolvePerContext(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    RegistrationWithStrategy.Resolve(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.ResolveObject(RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.Resolve(Type typeToResolve, ResolutionList resolutionPath, String name)
    <>c__DisplayClass71_0.<ResolveArguments>b__0(ParameterInfo p)
    SelectArrayIterator`2.ToArray()
    Enumerable.ToArray[TSource](IEnumerable`1 source)
    ObjectContainer.ResolveArguments(IEnumerable`1 parameters, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.CreateObject(Type type, ResolutionList resolutionPath, RegistrationKey keyToResolve)
    <>c__DisplayClass3_0.<ResolvePerContext>b__1()
    RegistrationWithStrategy.ExecuteWithLock(Object lockObject, Func`1 getter, Func`1 factory, ResolutionList resolutionPath)
    TypeRegistration.ResolvePerContext(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    RegistrationWithStrategy.Resolve(ObjectContainer container, RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.ResolveObject(RegistrationKey keyToResolve, ResolutionList resolutionPath)
    ObjectContainer.Resolve(Type typeToResolve, ResolutionList resolutionPath, String name)
    ObjectContainer.Resolve(Type typeToResolve, String name)
    TestObjectResolver.ResolveBindingInstance(Type bindingType, IObjectContainer container)
    lambda_method(Closure , IContextManager , Int32 )
    BindingInvoker.InvokeBinding(IBinding binding, IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration)
    TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments, TimeSpan& duration)
    TestExecutionEngine.ExecuteStep(IContextManager contextManager, StepInstance stepInstance)
    TestExecutionEngine.OnAfterLastStep()
    TestRunner.CollectScenarioErrors()
    CalculatorFeature.ScenarioCleanup()
    CalculatorFeature.AddTwoNumbers() line 13
gasparnagy commented 2 years ago

@shack05 The BeforeTestRun is executed only once, so it will access only the first TestThreadContainer (the documentation seems to be wrong BTW).

In order to save something to the TestThreadContainer, like you want, I think what you could do is to make a BeforeScenario hook with a very small order number (Order = -1), get the TestThreadContainer, check if the type was registered with container.IsRegistered and register to it if not.

An alternative approach is to wrap the TestThreadContainer with a per-scenario object, like what you can see in the SpecFlow MasterClass sample app here

gasparnagy commented 2 years ago

I close this issue now, but if my suggestions don't work, please feel free to reopen it.

SabotageAndi commented 2 years ago

🤦‍♂️no idea how I could have overlooked that. Thanks @gasparnagy!

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.