reqnroll / Reqnroll

Open-source Cucumber-style BDD test automation framework for .NET.
https://reqnroll.net
BSD 3-Clause "New" or "Revised" License
357 stars 37 forks source link

BoDi is no longer standalone #145

Closed UL-ChrisGlew closed 4 months ago

UL-ChrisGlew commented 4 months ago

Reqnroll Version

2.0.0

Which test runner are you using?

NUnit

Test Runner Version Number

N/A

.NET Implementation

.NET 8.0

Test Execution Method

ReSharper Test Runner

Content of reqnroll.json configuration file

No response

Issue Description

BoDi appears to no longer be a standalone NuGet package - forcing packages that used only the BoDi IObjectContainer to uptake the entire Reqnroll assembly.

This has broken many of our automation package/workflow and is a major blocker to future upgrades/development.

Ideally, this should have been implemented as a standalone NuGet package to allow for easier compatibility.

Steps to Reproduce

N/A

Link to Repro Project

No response

ajeckmans commented 4 months ago

This was done because it was very difficult to diagnose certain issues. Do you specifically need the exact same version as the reqnroll library uses or do you use it standalone?

gasparnagy commented 4 months ago

I think there are three use cases:

  1. If you have used BoDi independently from SpecFlow, just as a DI container. In this case you can keep using the BoDi package.

  2. If you used BoDi to prepare infrastructure for Reqnroll then using the Reqnroll package instead of the BoDi package is appropriate.

  3. If you have used BoDi as a DI for your application, but reused the same container building methods for preparing the Reqnroll tests then the solution might be a bit more tricky. In general our suggestion is not to use BoDi for the production code dependencies, but use one of the well-known DI solutions (e.g. Microsoft.Extensions.DependencyInjection, Autofac or Windsor) and use the related Reqnroll plugin to make those containers available for testing (if needed). As a temporary solution, you can also implement a simple Reqnroll plugin that forwards the Reqnroll object requests to the "old" BoDi. (You can use one of our plugins as an example.)

UL-ChrisGlew commented 4 months ago

In this specific use case, I have an automated testing framework that is used across multiple software products in our organisation and is designed to support both SpecFlow (for those projects that can't/won't update) and Reqnroll.

Under the previous v1.0.1 of Reqnroll, as both used the SpecFlow BoDi package this provided an easy migration path, as I did not need to change the IObjectContainer for use in each project. The problem is that, with this change, I will now have to choose to support either SpecFlow or Reqnroll for this framework, as it doesn't look like I can support both due to the integration of BoDi.

The framework project is also designed to support non-SpecFlow/Reqnroll projects, but using standalone containers that can be initialized manually on startup. I have reservations about including the full Reqnroll assembly for these projects.

For the above use case, what would you suggest is the correct next step? Looking at the example of forwarding, you would need to know every Assembly/Type being placed into the container - which we wouldn't necessarily know due to the scope of the framework.

gasparnagy commented 4 months ago

@UL-ChrisGlew Thx for the clarification. In your case having a separate package would not have helped, because we have made incompatible changes, so the version of BoDi Reqnroll needs would not be compatible with the version of BoDi SpecFlow uses anyway. But fortunately there is an easy solution.

As mentioned above for use case 3, you can make a Reqnroll plugin that forwards the object resolution requests to the object container you have created with the old BoDi. I have made a quick prototype and it seems to work fine (luckily once we integrated BoDi to Reqnroll we also changed the namespace (BoDi vs Reqnroll.BoDi), so they can be loaded side-by-side without issues).

So what you need to do is to add the following file to your Reqnroll project:

using Reqnroll.BoDi;
using Reqnroll.Infrastructure;
using Reqnroll.Plugins;
using Reqnroll.UnitTestProvider;
using ReqnrollPlugins.OldBoDi;

[assembly: RuntimePlugin(typeof(OldBoDiContainerPlugin))]

namespace ReqnrollPlugins.OldBoDi;

public class OldBoDiContainerPlugin : IRuntimePlugin
{
    public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration)
    {
        runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) =>
        {
            args.ObjectContainer.RegisterTypeAs<OldBoDiTestObjectResolver, ITestObjectResolver>();
        };

        runtimePluginEvents.CustomizeScenarioDependencies += (sender, args) =>
        {
            args.ObjectContainer.RegisterFactoryAs(() =>
            {
                // TODO: build your own container using the old BoDi here
                BoDi.IObjectContainer container = SetupTestDependencies.CreateServices();

                // register the new BoDi container to the old one, just in case...
                container.RegisterInstanceAs<IObjectContainer>(args.ObjectContainer);

                return container;
            });
        };
    }

    public class OldBoDiTestObjectResolver : ITestObjectResolver
    {
        public object ResolveBindingInstance(Type bindingType, IObjectContainer container)
        {
            var registered = IsRegistered(container, bindingType);

            return registered
                ? container.Resolve(bindingType)
                : container.Resolve<BoDi.IObjectContainer>().Resolve(bindingType);
        }

        private bool IsRegistered(IObjectContainer container, Type type)
        {
            if (container.IsRegistered(type))
            {
                return true;
            }

            // IObjectContainer.IsRegistered is not recursive, it will only check the current container
            if (container is ObjectContainer c && c.BaseContainer != null)
            {
                return IsRegistered(c.BaseContainer, type);
            }

            return false;
        }
    }
}

At the place that is marked with TODO, you need to somehow get the IObjectContainer you have build with the "old" BoDi. In my case I created a static method for this and called it from there. This static method can be in the those shared projects as well that are needed both by SpecFlow and Reqnroll, the method only depends on the old BoDi v1.5 package. In my example I have used that:

using BoDi;
using CalculatorApp;

namespace ReqnrollPlugins.OldBoDi.Support;
public class SetupTestDependencies
{
    public static IObjectContainer CreateServices()
    {
        var oldBoDiContainer = new ObjectContainer();

        oldBoDiContainer.RegisterTypeAs<TestCalculatorConfiguration, ICalculatorConfiguration>();
        oldBoDiContainer.RegisterTypeAs<Calculator, ICalculator>();

        return oldBoDiContainer;
    }
}

With this my sample project worked and I was able to load dependencies configured with "old" BoDi.

The magic is in the OldBoDiTestObjectResolver class from the plugin above. It basically checks if the object that needs to be resolved was configured in the Reqnroll's "new" BoDi container and if not, it resolves it from the "old" BoDi container you have produced.

Please let me know if that helps.

gasparnagy commented 4 months ago

I am closing this issue as by design now, but if the mentioned workaround does not work, please feel free to reopen.