cucumber / cucumber-jvm

Cucumber for the JVM
https://cucumber.io
MIT License
2.7k stars 2.02k forks source link

Awaitability.await() method unable to access ScenarioScope bean in cucumber-spring 6.10.2, it was working till cucumber-spring 4.8.1 #2647

Closed susnigdha1 closed 1 year ago

susnigdha1 commented 1 year ago

Hi, I am working on migrating our framework from cucumber 4.8.1 to 6.10.2, and we use Spring, Java, Junit, Webdriver. As part of this work, I have added @ScenarioScope annotation across our java classes (pageObjects and pageActions) which were earlier annotated with @Scope(SCOPE_CUCUMBER_GLUE). I am able to run the automation scripts, but the run fails as soon as code founds any await() method and the execution control enters into it. In the process await() creates a singleton bean and it cannot access any pageObjects that are scenario scoped. The error message says, ScopeNotActiveException. Given below sample code:

@Component
@ScenarioScope
public class pageObject{
    <some code is here>
}
@Component
@ScenarioScope
public class pageAction{
   @Autowired
    pageObject objObject;
   ...........
   await().atMost(Duration.ofSeconds(15)).until(objObject.isDisplayed());
}

Did anyone used Awaitability.await() in cucumber-spring v6.10.2 and above? Could you kindly guide me? We have been using Awaitability.await() with cucumber-spring 4.8.1 and that works.

mpkorstanje commented 1 year ago

In the process await() creates a singleton bean and it cannot access any pageObjects that are scenario scoped.

Assuming Awaitility waits on a different thread that sounds plausible. The CucumberTestContext is also bound to a thread. So creating a bean on a different thread could result in problems.

How did you establish a bean is being created only once objObjectis referenced (i.e when objObject.isDisplayed() is called)?

susnigdha1 commented 1 year ago

How did you establish a bean is being created only once objObject is referenced (i.e when objObject.isDisplayed() is called)? To answer it, in this case await() creates a singleton bean, and from within, it is trying to access objObject which is a ScenarioScope bean, as it is scenario scope the application context is having only 1 instance of the bean, considering tests are not running in parallel. My curiosity is cucumber-spring 4.8.1, we were using @scope(SCOPE_CUCUMBER_GLUE), and await() was working perfectly. So can you explain why cucumber-spring 6.x.x and above does not share state with another standard bean?

mpkorstanje commented 1 year ago

To answer it, in this case await() creates a singleton bean, and from within, it is trying to access objObject which is a ScenarioScope bean

But how did you establish this is in fact what happens? Can you explain what observations you made and how you reached your conclusion?

And were you able to see that this happened on a different thread?

Please bear in mind that you are trying to explain your problem to a complete stranger with a cell phone and some spare time while the food is in the oven. 😄

mpkorstanje commented 1 year ago

To reproduce the problem use:

public class ExampleSteps {

    @Autowired
    private ScenarioScopedContainer container;

    @Given("some step")
    public void some_step() {
        UUID s = container.getId();

        Awaitility.await().until(() -> {
            assertEquals(s, container.getId());
            return true;
        });
    }
}

The assertion will fail when used in combination with:

@ScenarioScope
@Component
public class ScenarioScopedContainer {

    private final UUID id = UUID.randomUUID();

    public UUID getId() {
        return id;
    }
}

However it will pass when using @Scope("cucumber-glue").

The difference between @ScenarioScope and @Scope("cucumber-glue") can be found in the source:

public @interface ScenarioScope {
    @AliasFor(
            annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

Because the ScopedProxyMode.TARGET_CLASS is used, each time a method on the ScenarioScopedContainer is used, a scenario scoped instance of it will be retrieved from the CucumberScenarioScope.

Combined with:

We end up attempting to create a new scope for awaitilities thread.

mpkorstanje commented 1 year ago

The motivation for using ScopedProxyMode.TARGET_CLASS can be found in https://github.com/cucumber/cucumber-jvm/pull/1974.

And this is also the solution to your problem. If your scenario scoped objects are only reachable through step definition classes and are not leaked into the application context you can either use @ScenarioScope(proxyMode = ScopedProxyMode.NO) or @Scope(CucumberTestContext.SCOPE_CUCUMBER_GLUE).

susnigdha1 commented 1 year ago

Hi @mpkorstanje, highly appreciate your time in analysing the problem in simple terms and recommending the solution with an example. However, there are some more challenges attached to it. Consider below given points first:

  1. In general, people use Maven Surefire plugin to run tests in parallel, using Junit4 and WebDriver. If they use Factory Design Pattern in Spring, then they need to annotate the Bean responsible for creating WebDriver with @ScenarioScope, to instantiate a new browser instance for the scenarios coming from different feature files for parallel run
  2. Additionally, if they use WebDriver PageFactory design pattern in their PageObject classes (annotated with @Componentand @ScenarioScope), and initialize the page factory using constructor, supplying the reference to the WebDriver bean created. And the web elements are declared with @FindBy. Now, look into the below issues:
  3. if we change @ScenarioScope proxy mode as per your recommendation in no 1 (above), then Cucumber will leak thread state and parallel run using Surefire will malfunction
  4. if we use await() on any PageObject' object from StepDefinition/PageActions class, it will still throw ScopeNotActive exception. Reason: The PageObject class is @ScenarioScoped and WebDriver PageFactory may create another thread to lazily retrieve the WebElement. if we change its ProxyMode or use the @Scope(CucumberTestContext.SCOPE_CUCUMBER_GLUE) then it will leak state and catastrophic result is a certainty. So, for large automation project, whose existing code uses a lot of await() and with(), migration to cucumber 6.10.2 and above going to involve huge effort in code refactoring. As, it is neither feasible, nor recommended, to create new classes with all objects that will be accessed only within Await(). FYI... if we create a local variable and store the proxy object into it within the same class file, and pass it within Await(), that also works.
mpkorstanje commented 1 year ago

It's a feature of Spring to reuse the Application context between tests. Not just for Cucumber, but also for example for JUnit. Until https://github.com/cucumber/cucumber-jvm/issues/1846#ref-pullrequest-541103690 Cucumber would override some of Springs internals to prevent this. But ultimately that was the wrong choice.

You may want to reconsider if Spring is the right dependency injection framework for you. If you aren't testing a Spring application cucumber-pico or cucumber-guice may be more suitable. Unlike Spring, with these frameworks each scenario has it's own.

if we change @ScenarioScope proxy mode as per your recommendation in no 1 (above), then Cucumber will leak thread state and parallel run using Surefire will malfunction

Could you provide a concrete example of this? Since these objects are only accessible through the already Scenario scoped step definitions it is not immediately apparent to me how any state would be leaked.

susnigdha1 commented 1 year ago

Hi, thanks a lot for your guidance and support. Sorry for delay in my response, my framework structure is almost same to this one. Differences are: cucumber version we are using is 6.10.2, and we are creating webdriver bean similarly, with @ScenarioScope. https://github.com/soraiareis/demo-spring-selenium

On Mon, 28 Nov 2022, 00:41 M.P. Korstanje, @.***> wrote:

It's a feature of Spring to reuse the Application context between tests. Not just for Cucumber, but also for example for JUnit. Until #1846 (reference) https://github.com/cucumber/cucumber-jvm/issues/1846#ref-pullrequest-541103690 Cucumber would override some of Springs internals to prevent this. But ultimately that was the wrong choice.

You may want to reconsider if Spring is the right dependency injection framework for you. If you aren't testing a Spring application cucumber-pico or cucumber-guice may be more suitable. Unlike Spring, with these frameworks each scenario has it's own.

if we change @ScenarioScope proxy mode as per your recommendation in no 1 (above), then Cucumber will leak thread state and parallel run using Surefire will malfunction

Could you provide a concrete example of this? Since these objects are only accessible through the already Scenario scoped step definitions it is not immediately apparent to me how any state would be leaked.

— Reply to this email directly, view it on GitHub https://github.com/cucumber/cucumber-jvm/issues/2647#issuecomment-1328388601, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFSLLM7LR2XQ7T4DYUMLJEDWKP5TTANCNFSM6AAAAAASLNDCUM . You are receiving this because you authored the thread.Message ID: @.***>