Closed 304NotModified closed 3 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?
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)
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
).
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
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.
I think
Replace(IServiceCollection, ServiceDescriptor)
inServiceCollectionDescriptorExtensions
provides that functionality.
Yes, but AFAIK you can't inject an IServiceCollection , only ServiceProvider which is readonly.
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) { }
...
}
in the steps of flow.feature , I like to inject IMyService with MyService and MyRepository , so
public static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
services.AddScoped<IMyService, MyService>();
services.AddScoped<IMyRepository, MyRepository>();
return services;
}
in the steps of actions.feature , I like to inject IMyService with MyService and mocked IMyRepository, like this, but now MyRepository registration should be removed!
public static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
services.AddScoped<IMyService, MyService>();
var myRepositoryMock = new Mock<IMyRepository>(); //Using Moq
services.AddSingleton<IMyRepository>(myRepositoryMock.Object);
//this should be disabled: services.AddScoped<IMyRepository, MyRepository>();
return services;
}
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;
}
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?).
Yes that's correct :)
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;
}
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!
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?