在 SpecFlow 3 生命周期挂钩中使用 SimpleInjector 作用域
Using SimpleInjector scopes in SpecFlow 3 life cycle hooks
我正在尝试在 .Net Core 2.1 Web 中设置我的 Specflow(V.3.0.155 测试版)测试 API 并且我正在遵循我在之前的一些 .Net 中使用的结构我参与过的 Framework Web API 个项目。
但是当我尝试解决我的依赖关系时,我 运行 遇到了麻烦,似乎我正在超出范围,所以当我尝试解决某些问题时,即我的测试数据上下文,我从 SpecFlow 得到以下错误:
Message: SimpleInjector.ActivationException : The registered delegate
for type
ProjectNexusContext threw an exception. The ProjectNexusContext is
registered as 'Async Scoped' lifestyle, but the instance is requested
outside the context of an active (Async Scoped) scope. ---->
SimpleInjector.ActivationException : The ProjectNexusContext is
registered as 'Async Scoped' lifestyle, but the instance is requested
outside the context of an active (Async Scoped) scope.
堆栈跟踪:
Result StackTrace: at SimpleInjector.InstanceProducer.GetInstance()
at SimpleInjector.Container.GetInstanceTService at
ProjectNexus.ApplicationContext.ResolveT in
C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus\ApplicationContext.cs:line
18 at ProjectNexus.Engine.Specs.LifecycleTestHooks.AfterStep() in
C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus.Engine.Specs\LifecycleTestHooks.cs:line
37 at
TechTalk.SpecFlow.Bindings.BindingInvoker.InvokeBinding(IBinding
binding, IContextManager contextManager, Object[] arguments,
ITestTracer testTracer, TimeSpan& duration) in
D:\a\s\TechTalk.SpecFlow\Bindings\BindingInvoker.cs:line 73 at
TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.InvokeHook(IBindingInvoker
invoker, IHookBinding hookBinding, HookType hookType) in
D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line
246 at
TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.FireEvents(HookType
hookType) in
D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line
232 at
TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(IContextManager
contextManager, StepInstance stepInstance) in
D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line
367 at
TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.Step(StepDefinitionKeyword
stepDefinitionKeyword, String keyword, String text, String
multilineTextArg, Table tableArg) in
D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line
475 at TechTalk.SpecFlow.TestRunner.Given(String text, String
multilineTextArg, Table tableArg, String keyword) in
D:\a\s\TechTalk.SpecFlow\TestRunner.cs:line 75 at
ProjectNexus.Engine.Specs.Tests.Client.Authentication.LogInFeature.FeatureBackground()
in
C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus.Engine.Specs\Tests\Client\Authentication\Log
In.feature:line 7 at
ProjectNexus.Engine.Specs.Tests.Client.Authentication.LogInFeature.LogInWithAValidUsernameAndPassword()
in
C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus.Engine.Specs\Tests\Client\Authentication\Log
In.feature:line 6
--ActivationException at SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration1
registration) at
SimpleInjector.Advanced.Internal.LazyScopedRegistration
1.GetInstance(Scope
scope) at lambda_method(Closure ) at
SimpleInjector.InstanceProducer.GetInstance() Result Message:
SimpleInjector.ActivationException : The registered delegate for type
ProjectNexusContext threw an exception. The ProjectNexusContext is
registered as 'Async Scoped' lifestyle, but the instance is requested
outside the context of an active (Async Scoped) scope. ---->
SimpleInjector.ActivationException : The ProjectNexusContext is
registered as 'Async Scoped' lifestyle, but the instance is requested
outside the context of an active (Async Scoped) scope. Result
StandardOutput: Given The following users are stored in the database
--- table step argument --- | Forneame | Surname | Username | | John | Smith | js001 |
-> error: The registered delegate for type ProjectNexusContext threw an exception. The ProjectNexusContext is registered as 'Async Scoped'
lifestyle, but the instance is requested outside the context of an
active (Async Scoped) scope.
我正在使用 Simple Injector 4.4.2 并在我的测试项目中设置我的容器
并在 BeforeTestRun SpecFlow 生命周期挂钩中调用的静态 class 方法中注册我的实例。
然后在我的 BeforeScenario 挂钩中,我将在所述容器上开始一个新的 AsyncScopedLifestyle 作用域,该作用域将在整个给定场景中使用。接下来,在我的 AfterScenario 挂钩中,我处理了这种生活方式。我查看了有关这种生活方式的 SimpleInjector 文档,我知道在活动范围的上下文之外会抛出此异常,但我不明白为什么我在活动上下文之外!
在检查 BeforeStep 挂钩中的 Scope 后,我可以看到,虽然没有释放,但 ScopeManager 中的 CurrentScope 属性 是空的,所以我显然不在活动范围上下文中。
这以前从来都不是问题,代码与前面提到的在测试项目中以完全相同的方式使用 SimpleInjector 的项目完全相同。我什至逐步检查了它们并检查了 BeforeStep 挂钩中的范围,并且 ScopeManager 中的 CurrentScope 是范围,因此它们显然没有超出范围。
我希望有人能看到我遗漏的东西,或者对如何解决这个问题提出一些建议。
我在下面包含了我的 SpecFlow 挂钩和 IoC 设置的代码:
SpecFlow 挂钩:
[Binding]
public class LifecycleTestHooks
{
[BeforeTestRun]
public static void BeforeTestRun()
{
TestIocConfiguration.Configure();
}
[BeforeScenario]
public void BeforeScenario()
{
TestIocConfiguration.StartExecutionScope();
}
[BeforeStep]
public void BeforeStep()
{
var projectDbContext = ApplicationContext.Resolve<ProjectNexusContext>();
projectDbContext.Database.BeginTransaction();
}
[AfterStep]
public void AfterStep()
{
var projectDbContext = ApplicationContext.Resolve<ProjectNexusContext>();
if (projectDbContext.Database.CurrentTransaction != null)
{
try
{
projectDbContext.Database.CommitTransaction();
}
catch (Exception)
{
projectDbContext.Database.RollbackTransaction();
throw;
}
}
TestIocConfiguration.CheckExecutionScope();
}
[AfterScenario]
public void AfterScenario()
{
TestIocConfiguration.EndExecutionScope();
}
}
IoC 设置
public class TestIocConfiguration
{
static Container container;
public static void Configure()
{
container = ApplicationContext.Container;
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
EngineInitialisation.Initialise();
container.Options.AllowOverridingRegistrations = true;
RegisterTestDatabaseContext();
RegisterTestApplicationConfiguration();
RegisterTestLdapConnectionService();
RegisterTestContext();
}
private static void RegisterTestDatabaseContext()
{
var testContext =
new ProjectNexusContext(new TestContextHelper().GetDbContextOptionsBuilder());
testContext.Database.OpenConnection();
testContext.Database.EnsureCreated();
container.Register<ProjectNexusContext>(() => testContext, Lifestyle.Scoped);
}
private static void RegisterTestLdapConnectionService()
{
container.Register<ILdapConnectionService, TestLdapConnectionService>(Lifestyle.Scoped);
}
private static void RegisterTestApplicationConfiguration()
{
var appConfig = new ApplicationConfiguration
{
LdapHost = "",
LdapPort = 0,
ApplicationSecret = "TestSecret",
TokenExpirationDays = 1
};
container.Register<ApplicationConfiguration>(() => appConfig, Lifestyle.Scoped);
}
private static void RegisterTestContext()
{
container.Register<TestContext>(Lifestyle.Singleton);
}
public static Scope StartExecutionScope()
{
return AsyncScopedLifestyle.BeginScope(container);
}
public static void EndExecutionScope()
{
Lifestyle.Scoped.GetCurrentScope(container)?.Dispose();
}
}
应用程序上下文
public class ApplicationContext
{
public static readonly Container Container;
public static readonly Mapper Mapper;
static ApplicationContext()
{
Container = new Container();
}
public static T Resolve<T>() where T : class
{
return Container.GetInstance<T>();
}
public static Cast Resolve<T, Cast>()
where Cast : T
where T : class
{
return (Cast)Container.GetInstance<T>();
}
}
在每个规范之前,您需要创建一个新的 Container 以供使用,因为我认为重复使用不会达到您对当前设置的期望。
您可以在 ApplicationContext
中添加一个名为 SetContainer
的方法,如下所示:
public static void SetContainer(Container container)
{
Container = container;
}
然后在你的 BeforeScenario
钩子中你可以 运行 你的 IoC 配置,但是新建一个容器并使用你的 SetContainer
方法
更新:似乎在 Specflow 3-beta 中,活动范围在离开测试挂钩方法(例如 BeforeScenario)后立即被处理
现在您可以使用单例生活方式注册您的实例,并为每个测试使用一个新容器(就像您可以按照我上面提出的建议那样做)。
我在 Specflow 2.3.2 中尝试了相同的代码,它工作正常,但在 Specflow 3-beta 中出现问题。
我正在尝试在 .Net Core 2.1 Web 中设置我的 Specflow(V.3.0.155 测试版)测试 API 并且我正在遵循我在之前的一些 .Net 中使用的结构我参与过的 Framework Web API 个项目。
但是当我尝试解决我的依赖关系时,我 运行 遇到了麻烦,似乎我正在超出范围,所以当我尝试解决某些问题时,即我的测试数据上下文,我从 SpecFlow 得到以下错误:
Message: SimpleInjector.ActivationException : The registered delegate for type ProjectNexusContext threw an exception. The ProjectNexusContext is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope. ----> SimpleInjector.ActivationException : The ProjectNexusContext is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope.
堆栈跟踪:
Result StackTrace: at SimpleInjector.InstanceProducer.GetInstance()
at SimpleInjector.Container.GetInstanceTService at ProjectNexus.ApplicationContext.ResolveT in C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus\ApplicationContext.cs:line 18 at ProjectNexus.Engine.Specs.LifecycleTestHooks.AfterStep() in C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus.Engine.Specs\LifecycleTestHooks.cs:line 37 at TechTalk.SpecFlow.Bindings.BindingInvoker.InvokeBinding(IBinding binding, IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration) in D:\a\s\TechTalk.SpecFlow\Bindings\BindingInvoker.cs:line 73 at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.InvokeHook(IBindingInvoker invoker, IHookBinding hookBinding, HookType hookType) in D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line 246 at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.FireEvents(HookType hookType) in D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line 232 at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(IContextManager contextManager, StepInstance stepInstance) in D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line 367 at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.Step(StepDefinitionKeyword stepDefinitionKeyword, String keyword, String text, String multilineTextArg, Table tableArg) in D:\a\s\TechTalk.SpecFlow\Infrastructure\TestExecutionEngine.cs:line 475 at TechTalk.SpecFlow.TestRunner.Given(String text, String multilineTextArg, Table tableArg, String keyword) in D:\a\s\TechTalk.SpecFlow\TestRunner.cs:line 75 at ProjectNexus.Engine.Specs.Tests.Client.Authentication.LogInFeature.FeatureBackground() in C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus.Engine.Specs\Tests\Client\Authentication\Log In.feature:line 7 at ProjectNexus.Engine.Specs.Tests.Client.Authentication.LogInFeature.LogInWithAValidUsernameAndPassword() in C:\Users\dawes\source\repos\ProjectNexus\ProjectNexus.Engine.Specs\Tests\Client\Authentication\Log In.feature:line 6 --ActivationException at SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration1 registration) at SimpleInjector.Advanced.Internal.LazyScopedRegistration
1.GetInstance(Scope scope) at lambda_method(Closure ) at SimpleInjector.InstanceProducer.GetInstance() Result Message: SimpleInjector.ActivationException : The registered delegate for type ProjectNexusContext threw an exception. The ProjectNexusContext is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope. ----> SimpleInjector.ActivationException : The ProjectNexusContext is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope. Result StandardOutput: Given The following users are stored in the database --- table step argument --- | Forneame | Surname | Username | | John | Smith | js001 | -> error: The registered delegate for type ProjectNexusContext threw an exception. The ProjectNexusContext is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope.
我正在使用 Simple Injector 4.4.2 并在我的测试项目中设置我的容器 并在 BeforeTestRun SpecFlow 生命周期挂钩中调用的静态 class 方法中注册我的实例。
然后在我的 BeforeScenario 挂钩中,我将在所述容器上开始一个新的 AsyncScopedLifestyle 作用域,该作用域将在整个给定场景中使用。接下来,在我的 AfterScenario 挂钩中,我处理了这种生活方式。我查看了有关这种生活方式的 SimpleInjector 文档,我知道在活动范围的上下文之外会抛出此异常,但我不明白为什么我在活动上下文之外!
在检查 BeforeStep 挂钩中的 Scope 后,我可以看到,虽然没有释放,但 ScopeManager 中的 CurrentScope 属性 是空的,所以我显然不在活动范围上下文中。
这以前从来都不是问题,代码与前面提到的在测试项目中以完全相同的方式使用 SimpleInjector 的项目完全相同。我什至逐步检查了它们并检查了 BeforeStep 挂钩中的范围,并且 ScopeManager 中的 CurrentScope 是范围,因此它们显然没有超出范围。
我希望有人能看到我遗漏的东西,或者对如何解决这个问题提出一些建议。
我在下面包含了我的 SpecFlow 挂钩和 IoC 设置的代码:
SpecFlow 挂钩:
[Binding]
public class LifecycleTestHooks
{
[BeforeTestRun]
public static void BeforeTestRun()
{
TestIocConfiguration.Configure();
}
[BeforeScenario]
public void BeforeScenario()
{
TestIocConfiguration.StartExecutionScope();
}
[BeforeStep]
public void BeforeStep()
{
var projectDbContext = ApplicationContext.Resolve<ProjectNexusContext>();
projectDbContext.Database.BeginTransaction();
}
[AfterStep]
public void AfterStep()
{
var projectDbContext = ApplicationContext.Resolve<ProjectNexusContext>();
if (projectDbContext.Database.CurrentTransaction != null)
{
try
{
projectDbContext.Database.CommitTransaction();
}
catch (Exception)
{
projectDbContext.Database.RollbackTransaction();
throw;
}
}
TestIocConfiguration.CheckExecutionScope();
}
[AfterScenario]
public void AfterScenario()
{
TestIocConfiguration.EndExecutionScope();
}
}
IoC 设置
public class TestIocConfiguration
{
static Container container;
public static void Configure()
{
container = ApplicationContext.Container;
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
EngineInitialisation.Initialise();
container.Options.AllowOverridingRegistrations = true;
RegisterTestDatabaseContext();
RegisterTestApplicationConfiguration();
RegisterTestLdapConnectionService();
RegisterTestContext();
}
private static void RegisterTestDatabaseContext()
{
var testContext =
new ProjectNexusContext(new TestContextHelper().GetDbContextOptionsBuilder());
testContext.Database.OpenConnection();
testContext.Database.EnsureCreated();
container.Register<ProjectNexusContext>(() => testContext, Lifestyle.Scoped);
}
private static void RegisterTestLdapConnectionService()
{
container.Register<ILdapConnectionService, TestLdapConnectionService>(Lifestyle.Scoped);
}
private static void RegisterTestApplicationConfiguration()
{
var appConfig = new ApplicationConfiguration
{
LdapHost = "",
LdapPort = 0,
ApplicationSecret = "TestSecret",
TokenExpirationDays = 1
};
container.Register<ApplicationConfiguration>(() => appConfig, Lifestyle.Scoped);
}
private static void RegisterTestContext()
{
container.Register<TestContext>(Lifestyle.Singleton);
}
public static Scope StartExecutionScope()
{
return AsyncScopedLifestyle.BeginScope(container);
}
public static void EndExecutionScope()
{
Lifestyle.Scoped.GetCurrentScope(container)?.Dispose();
}
}
应用程序上下文
public class ApplicationContext
{
public static readonly Container Container;
public static readonly Mapper Mapper;
static ApplicationContext()
{
Container = new Container();
}
public static T Resolve<T>() where T : class
{
return Container.GetInstance<T>();
}
public static Cast Resolve<T, Cast>()
where Cast : T
where T : class
{
return (Cast)Container.GetInstance<T>();
}
}
在每个规范之前,您需要创建一个新的 Container 以供使用,因为我认为重复使用不会达到您对当前设置的期望。
您可以在 ApplicationContext
中添加一个名为 SetContainer
的方法,如下所示:
public static void SetContainer(Container container)
{
Container = container;
}
然后在你的 BeforeScenario
钩子中你可以 运行 你的 IoC 配置,但是新建一个容器并使用你的 SetContainer
方法
更新:似乎在 Specflow 3-beta 中,活动范围在离开测试挂钩方法(例如 BeforeScenario)后立即被处理
现在您可以使用单例生活方式注册您的实例,并为每个测试使用一个新容器(就像您可以按照我上面提出的建议那样做)。
我在 Specflow 2.3.2 中尝试了相同的代码,它工作正常,但在 Specflow 3-beta 中出现问题。