Skip to content

CucumberException: No qualifying bean of type 'Steps' available: expected single matching bean but found 1: Steps #1823

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
noel-yap opened this issue Nov 19, 2019 · 2 comments
Labels
🐛 bug Defect / Bug

Comments

@noel-yap
Copy link

noel-yap commented Nov 19, 2019

When using a custom application loader, expected single matching bean but found 1 is emitted. This behavior is non-deterministic but it seems sticky (I'm guessing there's some caching going on).

We have something like the following code:

public class SharedApplicationContextLoader extends SpringBootContextLoader {
  @SpringBootApplication
  static class ApplicationContextSourceFallback {
  }

  private static final AtomicReference<Supplier<ApplicationContext>> applicationContextSupplier = new AtomicReference<>(null);
  private static final AtomicReference<ApplicationContext> applicationContext = new AtomicReference<>(null);

  public static synchronized void setApplicationContext(final ApplicationContext applicationContext) {
    setApplicationContext(() -> applicationContext);
  }

  private static synchronized void setApplicationContext(final Supplier<ApplicationContext> applicationContextSupplier) {
    SharedApplicationContextLoader.applicationContextSupplier.compareAndSet(null, applicationContextSupplier);

    SharedApplicationContextLoader.applicationContext.compareAndSet(
        null,
        SharedApplicationContextLoader.applicationContextSupplier.get().get());
  }

  @Override
  public ApplicationContext loadContext(final MergedContextConfiguration config) {
    SharedApplicationContextLoader.setApplicationContext(() -> {
      try {
        return super.loadContext(config);
      } catch (final Exception e) {
        throw new RuntimeException(e);
      }
    });

    return applicationContext.get();
  }

  @Override
  protected SpringApplication getSpringApplication() {
    return new SpringApplication(ApplicationContextSourceFallback.class);
  }
}
@SpringBootTest
@ContextConfiguration(
    classes = TestsConfiguration.class,
    loader = SharedApplicationContextLoader.class)
public class Steps {
}
@SpringBootApplication
@ContextConfiguration(classes = TestsConfiguration.class)
public class Main {
  public static void main(String[] args) {
    final ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);

    SharedApplicationContextLoader.setApplicationContext(applicationContext);
  }
}

Expected behavior
No exception thrown.

Context & Motivation
During development, the tests are run directly from the IDE. Once committed and pushed, an app is built and deployed housing the tests and will execute them.

Stacktrace

      cucumber.runtime.CucumberException: No qualifying bean of type 'com.netflix.consors.crucible.SubscriptionTokenTest$Steps' available: expected single matching bean but found 1: com.netflix.consors.crucible.SubscriptionTokenTest$Steps
	at io.cucumber.spring.SpringFactory.getInstance(SpringFactory.java:213)
	at cucumber.runtime.java.ObjectFactoryLoader$ObjectFactoryAdapter.getInstance(ObjectFactoryLoader.java:157)
	at cucumber.runtime.java.JavaHookDefinition.execute(JavaHookDefinition.java:65)
	at cucumber.runner.HookDefinitionMatch.runStep(HookDefinitionMatch.java:16)
	at cucumber.runner.TestStep.executeStep(TestStep.java:65)
	at cucumber.runner.TestStep.run(TestStep.java:50)
	at cucumber.runner.TestCase.run(TestCase.java:42)
	at cucumber.runner.Runner.runPickle(Runner.java:50)
	at io.cucumber.junit.PickleRunners$NoStepDescriptions.run(PickleRunners.java:146)
	at io.cucumber.junit.FeatureRunner.runChild(FeatureRunner.java:68)
	at io.cucumber.junit.FeatureRunner.runChild(FeatureRunner.java:23)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at io.cucumber.junit.Cucumber.runChild(Cucumber.java:142)
	at io.cucumber.junit.Cucumber.runChild(Cucumber.java:65)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at io.cucumber.junit.Cucumber$RunCucumber.evaluate(Cucumber.java:172)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
	at com.netflix.crucible.platform.JUnitTestRunner.invokeTest(JUnitTestRunner.java:32)
	at com.netflix.crucible.platform.GenericTestRunner.runTestCore(GenericTestRunner.java:81)
	at com.netflix.crucible.platform.GenericTestRunner.runTest(GenericTestRunner.java:51)
	at com.netflix.crucible.grpc.CrucibleServiceImpl.runTest(CrucibleServiceImpl.java:61)
	at com.netflix.crucible.protogen.CrucibleServiceGrpc$MethodHandlers.invoke(CrucibleServiceGrpc.java:309)
	at io.grpc.stub.ServerCalls$UnaryServerCallHandler$UnaryServerCallListener.onHalfClose(ServerCalls.java:171)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at com.netflix.grpc.interceptor.error.ErrorCatchingServerInterceptor$3.onHalfClose(ErrorCatchingServerInterceptor.java:177)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at com.netflix.grpc.interceptor.logging_context.LoggingContextServerInterceptor$1.lambda$onHalfClose$2(LoggingContextServerInterceptor.java:75)
	at com.netflix.grpc.interceptor.logging_context.LoggingContextServerInterceptor$1.callWithMdc(LoggingContextServerInterceptor.java:108)
	at com.netflix.grpc.interceptor.logging_context.LoggingContextServerInterceptor$1.onHalfClose(LoggingContextServerInterceptor.java:75)
	at brave.grpc.TracingServerInterceptor$ScopingServerCallListener.onHalfClose(TracingServerInterceptor.java:138)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at com.netflix.grpc.interceptor.request_context_bridge.RequestContextBridgeServerInterceptor$1.lambda$onHalfClose$1(RequestContextBridgeServerInterceptor.java:82)
	at com.netflix.lang.BindingContexts.runWithContext(BindingContexts.java:251)
	at com.netflix.grpc.interceptor.request_context_bridge.RequestContextBridgeServerInterceptor$1.onHalfClose(RequestContextBridgeServerInterceptor.java:82)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at io.grpc.PartialForwardingServerCallListener.onHalfClose(PartialForwardingServerCallListener.java:35)
	at io.grpc.ForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:23)
	at io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose(ForwardingServerCallListener.java:40)
	at com.netflix.grpc.interceptor.error.ErrorStatusAugmentingServerInterceptor$2.onHalfClose(ErrorStatusAugmentingServerInterceptor.java:54)
	at io.grpc.internal.ServerCallImpl$ServerStreamListenerImpl.halfClosed(ServerCallImpl.java:283)
	at io.grpc.internal.ServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed.runInContext(ServerImpl.java:707)
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'Steps' available: expected single matching bean but found 1: Steps
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1148)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:413)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:346)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:339)
	at io.cucumber.spring.SpringFactory.getInstance(SpringFactory.java:211)
	... 74 more

Your Environment

  • Versions used: 4.8.0
  • Operating System and version: Ubuntu 18.04.3 LTS
  • Build tool: Gradle 5.6.4

Additional context
I've only seen the issue exhibit in a Docker container.

@noel-yap noel-yap added the 🐛 bug Defect / Bug label Nov 19, 2019
@mpkorstanje
Copy link
Contributor

mpkorstanje commented Nov 19, 2019

From what you've provided I honestly wouldn't be able to say what the problem is here. If you can provide it as an MCVE that would be much appreciated.

Some guesses that you may want to investigate yourself:

  1. Could you check if there is a proxy around Steps. Something makes it so that the existing bean is not a valid match. Perhaps you'll have to attach a debugger to see why Spring doesn't consider the bean it found to be valid.
cucumber.runtime.CucumberException: No qualifying bean of type 'com.netflix.consors.crucible.SubscriptionTokenTest$Steps' available: expected single matching bean but found 1: com.netflix.consors.crucible.SubscriptionTokenTest$Steps
  1. Are you executing Cucumber more then once and/or in parallel? The fragment below makes this a possibility.

Cucumber Spring can not share the application context between different test executions. This would cause concurrent modifications to the application context (all beans within the cucumber-glue scope). Though I would have expected a different error in this case.

	at io.grpc.internal.ServerCallImpl$ServerStreamListenerImpl.halfClosed(ServerCallImpl.java:283)
	at io.grpc.internal.ServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed.runInContext(ServerImpl.java:707)
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
  1. Does the problem still occur if you do not share the application context between the application and Cucumber executions? Removing the SharedApplicationContextLoader will result in a higher memory consumption but should also isolate the application from the test context.

  2. Does this problem occur if you run the docker application locally and invoke the tests as they would be in production?

This behavior is non-deterministic but it seems sticky (I'm guessing there's some caching going on)

  1. There is caching going on. Test application contexts are cached by cucumber spring. We're using the same mechanism as springs test context manager would but had to make it thread-safe (a less then ideal solution, but we're kinda stuck on that atm see: https://github.com/cucumber/cucumber-jvm/blob/master/spring/src/main/java/io/cucumber/spring/FixBootstrapUtils.java)

After each scenario the cucumber-glue scope is cleaned though. So I don't think that is relevant for your exception.

@mpkorstanje
Copy link
Contributor

I'll close this for now. Please feel free to reopen if you can provide more information.

mpkorstanje pushed a commit that referenced this issue Dec 21, 2019
When executing Cucumber in parallel in combination with `cucumber-spring` users
would often encounter a cryptic error message: "No qualifying bean of type
'Steps' available: expected single matching bean but found 1" (#1823).

This was initially fixed by ensuring that the Spring application context was not
shared between threads (#1153, #1148). This however has the downside that the
application context is not shared with JUnit tests either (#1583) and comes at
significant performance penalty (#1846).

The root cause however was a race condition in
`DefaultListableBeanFactory.registerBeanDefinition(String beanName, BeanDefinition beanDefinition)`.
There is a check to see if the definition already exists. If it does not exist,
it replaces the bean definition in a ConcurrentHashMap (in a synchronized block
as well) but then adds the bean name to a list without checking if it already
exists.

By synchronizing inside `SpringFactory.start` we prevent concurrent modification
of the application context by `cucumber-spring` This in turn makes it possible
to share the application context between threads and with JUnit tests.

Fixes: #1846
Fixes: #1583
Closes: #1848
Closes: #1582
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛 bug Defect / Bug
Projects
None yet
Development

No branches or pull requests

2 participants