solidtoken / SpecFlow.DependencyInjection

SpecFlow plugin that enables to use Microsoft.Extensions.DependencyInjection for resolving test dependencies.
BSD 3-Clause "New" or "Revised" License
35 stars 11 forks source link

Feature request: add to ServiceCollection depending of feature or scenario #29

Closed 304NotModified closed 3 years ago

304NotModified commented 4 years ago

I would be cool if we could add some registrations to the ServiceCollection depending of feature or scenario.

Or is this already possible? If so, how could I use it?

mbhoek commented 4 years ago

Sorry for not getting back at you earlier, crazy times.

The 'registrations' are setup before the steps are executed (i.e. BuildServiceProvider in the DependencyInjectionPlugin), so I don't think you can change those afterwards.

It is possible to change the behavior in code using the [BeforeFeature]/[BeforeScenario] hooks, as described in 'Advanced Options' on https://specflow.org/documentation/Context-Injection/ (note: those are executed for all features/scenarios, not just a specific one).

Counter-question: what is it that you would like to achieve? Could you come up with an example that helps me better understand?

304NotModified commented 4 years ago

Indeed craze times! No problem, better late than never ;)

We sometimes need other mocks, for example, some scenarios are integration tests (not mocked), and others are mocked (for performance and stability).

Now we need to trick that or use 2 projects.

It is possible to change the behavior in code using the [BeforeFeature]/[BeforeScenario] hooks, as described in 'Advanced Options' on https://specflow.org/documentation/Context-Injection/ (note: those are executed for all features/scenarios, not just a specific one).

I guess that's fine, if I know the scenario. Maybe a new attribute for that then? e.g. [FeatureDependencies] and [AllScenarioDependencies]? (the latter is the current global)

mbhoek commented 4 years ago

I don't want to introduce functionality which allows Features or Steps to become aware that they are running a mocked or non-mocked version of the test; I think that goes against the nature of dependency injection. Also, I can imagine the same problem exist for people using other plugins (e.g. Autofac or Ninject) so that leads me to believe that this should be solved in Specflow rather than just in this particular plugin.

Information about the Scenario can be found using ScenarioContext (which is also injected). Read more about it at https://specflow.org/documentation/ScenarioContext/ (specifically ScenarioInfo).

304NotModified commented 4 years ago

Well the difference between Autofac/ninject and ServiceCollection is that in the first libraries you could add/replace dependencies and easy create child containers. That's not the case with ServiceCollection

mbhoek commented 4 years ago

I think Replace(IServiceCollection, ServiceDescriptor) in ServiceCollectionDescriptorExtensions provides that functionality.

Although it's probably easier to register different services based upon what you want to test (mocked vs integration) in the CreateServices() method. Or have separate projects, like you already suggested. It's a bit hard to tell without knowing your specific implementation 🤔

I'll give it some more thought because I do find it an interesting use case; I'm just not sure if there's anything I can add to this plugin which can help. I'll leave the issue open in the mean time, maybe we can get some feedback from other users as well.

304NotModified commented 4 years ago

I think Replace(IServiceCollection, ServiceDescriptor) in ServiceCollectionDescriptorExtensions provides that functionality.

Yes, but AFAIK you can't inject an IServiceCollection , only ServiceProvider which is readonly.

See also https://stackoverflow.com/questions/53580470/how-do-you-build-a-servicecollection-out-of-a-serviceprovider

It's a bit hard to tell without knowing your specific implementation

I will try to explain better :)

For now, we have to write custom factories and builders as we have only 1 servicecollection/serviceprovider for a project. This is only needed in the feature file project. I would be nice if we could skip that.

For example, I have these interface and classes:

interface IMyRepository
{
    ...
}

class MyRepository : IMyRepository
{
    ...
}

interface IMyService
{
    ...
}

class MyService : IMyService
{
    public MyService(IMyRepository myRepository) { }

    ...
}

So i'm looking for something like this:

[ScenarioDependencies]
public static IServiceCollection CreateServices(ScenarioContext context)
{
    var services = new ServiceCollection();

    services.AddScoped<IMyService, MyService>();
    if (context.Scenario == "action")
    {
        var myRepositoryMock = new Mock<IMyRepository>(); //Using Moq
        services.AddSingleton<IMyRepository>(myRepositoryMock.Object);
    }
    else
    {
        services.AddScoped<IMyRepository, MyRepository>();
    }

    return services;
}
mbhoek commented 4 years ago

Thanks for the eloborate example. It helps because I was thinking you wanted to use the same (1) Feature/Scenario to test both mocked and non-mocked versions. Do I understand correctly that you are okay with having separate features/scenarios (so one feature for mocked and one feature for non-mocked?).

304NotModified commented 4 years ago

Yes that's correct :)

EjaYF commented 3 years ago

I think this is already possible using Factory registration, at least, in my implementation I have a similar issue working.

The idea is to register the implementation itself directly, and when registering the interface of the implementation to use a factory which first retrieves the ScenarioContext. Based on something in the ScenarioContext you then can return different implementations for this interface.

Pseudo-code based on the example of @304NotModified:

[ScenarioDependencies]
public static IServiceCollection CreateServices()
{
    var services = new ServiceCollection();

    services.AddScoped<IMyService, MyService>();

    // register the implementation without it's interface
    services.AddScoped<MyRepository>();

    services.AddScoped<IMyRepository>(ctx => {
        var scenarioContext = ctx.GetRequiredService<ScenarioContext>();
        if (context.Scenario == "action")
        {
            var myRepositoryMock = new Mock<IMyRepository>(); //Using Moq
            return myRepositoryMock.Object;
        }
        else
        {
            return ctx.GetRequiredService<MyRepository>();
        }
    });

    return services;
}
304NotModified commented 3 years ago

Ow cool!

Well I think we could close this one then.

I don't have an project to test this now. I'm not working anymore where we needed this.

Anyway thx, maybe I will try this in the future!