Skip to content
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

Impossible to decorate Page fields for multiple drivers #221

Closed
FiMka opened this issue Jul 25, 2015 · 10 comments
Closed

Impossible to decorate Page fields for multiple drivers #221

FiMka opened this issue Jul 25, 2015 · 10 comments

Comments

@FiMka
Copy link

FiMka commented Jul 25, 2015

Hello!
What is described below isn't an issue (it's by design), but could you possibly suggest something to solve my issue.
My tests use @AndroidFindBy annotations to inject WebElements. My test cases involve multiple devices, so I use selenium grid approach. This implies creating of multiple AndroidDriver instances.
Could you suggest how to decorate Page objects when using several drivers?

PageFactory.initElements(new AppiumFieldDecorator(driver,
    MAX_WAIT_FOR_ELEMENT_SEC, TimeUnit.SECONDS), bean);
@TikhomirovSergey
Copy link
Contributor

Hi @FiMka

I'm trying to fugure out your problem.
Am I correcly understand that you create a several drivers at one test session (e.g. one test method opens few Appium sessions and checks your app on few devices/emulators at an one time)? If it is true why is it not the parallel testing?

Ok. If you have to use several drivers at one test method then you shoul create an instance of AppiumFieldDecorator for each one driver.

driver1 = new AndroidDriver<>(constructor parameters) ;
driver2 = new AndroidDriver<>(constructor parameters) ;
//

AppiumFieldDecorator afd1 = new AppiumFieldDecorator(driver1,
    MAX_WAIT_FOR_ELEMENT_SEC, TimeUnit.SECONDS);

AppiumFieldDecorator afd2 = new AppiumFieldDecorator(driver2,
    MAX_WAIT_FOR_ELEMENT_SEC, TimeUnit.SECONDS);

//your code
//lots of code :)
//

PageFactory.initElements(afd1, bean1);
PageFactory.initElements(afd2, bean2);

//or if your page object is the same for all drivers
//
PageFactory.initElements(afd1, bean);
PageFactory.initElements(afd2, bean);

If you have the parallel testing or you are able to organize it (the same test method is being run with different parameters in different threads at an one time). The code sample above becomes much simplified.

@TikhomirovSergey
Copy link
Contributor

It would be cool if there was a method like that on Selenium side:

PageFactory.initElements(searchContext, decoratorInstance, pageObjectInstance);

PageFactory has methods that you can look at if you'll go to link. Selenium PageFactory

@FiMka
Copy link
Author

FiMka commented Jul 25, 2015

Hi Sergey! I do really appreciate your assistance in solving my problem!

As Sergey Tikhomirov said: Am I correcly understand that you create a several drivers at one test session (e.g. one test method opens few Appium sessions and checks your app on few devices/emulators at an one time)? If it is true why is it not the parallel testing?

You understand this absolutely right. I can give a simple example when this can be needed: Two devices are chatting with each other. This use case is real close to what I have. With parallel testing the synchronization between devices may be more complicated.

As Sergey Tikhomirov said: Ok. If you have to use several drivers at one test method then you should create an instance of AppiumFieldDecorator for each one driver.

Thank you! I'll definitely try this out.

As Sergey Tikhomirov said: PageFactory has methods that you can if you'll go to link. Selenium PageFactory

Got it, thanks. This is what I use right now.

@FiMka
Copy link
Author

FiMka commented Jul 25, 2015

As Sergey Tikhomirov said: Ok. If you have to use several drivers at one test method then you should create an instance of AppiumFieldDecorator for each one driver.

I've just remembered that I tried this already as follows:

  @Override
  public Object postProcessBeforeInitialization(final Object bean, final String beanName)
      throws BeansException {
    if (bean.getClass().isAnnotationPresent(Page.class)) {
      for (final AndroidDriver driver : drivers) {
        PageFactory.initElements(new AppiumFieldDecorator(driver, MAX_WAIT_FOR_ELEMENT_SEC,
          TimeUnit.SECONDS), bean);
      }
    }
    return bean; // return decorated page
  }

I use Spring in my project, all Page objects (represented as Spring beans) are decorated in postProcessBeforeInitialization (in a class implementing BeanPostProcessor). So each page bean is decorated only with latest driver from the list. Because Spring will return a single decorated page bean after post processing. I hope I'm explaining clear.

It seems I have to throw out Spring from my project or forget about @AndroidFindBy annotations.

@TikhomirovSergey
Copy link
Contributor

It seems that you are doing something overcomplicated. I don't think that there it is needed to throw out Spring or Appium annotations...

if (bean.getClass().isAnnotationPresent(Page.class)) {
      for (final AndroidDriver driver : drivers) {
        PageFactory.initElements(new AppiumFieldDecorator(driver, MAX_WAIT_FOR_ELEMENT_SEC,
          TimeUnit.SECONDS), bean);
      }
    }

I'm seeing that here will be used only the last instance anyway. + Here should be a problem with performance. Duration of postprocessing execution increases with driver set.

Why is it not possible to decorate a created bean or a real page object before it is proxied?

Please read about the page object design pattern. All is simple. It is just a wrapper of a search contex with lazy instantiated fields.

If there was a method like that

PageFactory.initElements(searchContext, decoratorInstance, pageObjectInstance);

you would have the same promlem.

@TikhomirovSergey
Copy link
Contributor

Ok. If it is needed to use this instantiation then postprocessing should know which driver is "free". Is it possible to use only the last element of the driver collection? Maybe you have to store something like a map where keys are drivers that are already "busy". I think there are many ways to resolve this problem...

@FiMka
Copy link
Author

FiMka commented Jul 25, 2015

Sergey, let me rephrase my question. If I have a page like:

public class SomePage {

  @AndroidFindBy(id = PACKAGE + ":id/someButton”)
  private WebElement someButton;

}

And this page is decorated for each one driver (like you suggested). How to "switch" between drivers (devices) during testing? Because when using @AndroidFindBy annotations I don't operate anymore with driver directly in the tests. I use this once only - when initializing (injecting) page fields (web elements).

@TikhomirovSergey
Copy link
Contributor

Ah. Now your question is clear, I think...

So when you create drivers

driver1 = new AndroidDriver<>(URL of a machine where Appium server 1 is launced, capabilities) ;
driver2 = new AndroidDriver<>(URL of a machine where Appium server 2 is launced, capabilities) ;

two different sessions are being created. Each one session is related to the certain server. Just because appium server doesn't work with two or more devices/emulators at the same time so each one session is linked with the certain device/emulator.

When you are performing the following

AppiumFieldDecorator afd1 = new AppiumFieldDecorator(driver1,
    MAX_WAIT_FOR_ELEMENT_SEC, TimeUnit.SECONDS);

AppiumFieldDecorator afd2 = new AppiumFieldDecorator(driver2,
    MAX_WAIT_FOR_ELEMENT_SEC, TimeUnit.SECONDS);

//your code
//lots of code :)
//

PageFactory.initElements(afd1, bean1);
PageFactory.initElements(afd2, bean2);

you are populating fields of two different instances (bean1, bean2) using two different drivers (driver1 and driver2).

Ok.

public class SomePage {

  @AndroidFindBy(id = PACKAGE + ":id/someButton”)
  private WebElement someButton; //it is not a real element. It is proxy object created by CGLib.
  //It wraps the certain search context. When you are trying to do something with element 
  //then:
  //- it performs the searching
  // - invokes the method of the real element that has been found

  public void clickSomeButton(){
      someButton.click(); //here the real element is being found and click is being berformed
  } 

}

So. There is nothing to do something special in order to switch between devices.

bean1.clickSomeButton(); //perfoms the action on device 1/ emilator 1
bean2.clickSomeButton(); //perfoms the action on device 2/ emilator 2

I think it is all. If there are more questions you are free to ask :)

@FiMka
Copy link
Author

FiMka commented Jul 26, 2015

Sergey, thanks for the clarification!

Related to this:

As Sergey Tikhomirov said: So. There is nothing to do something special in order to switch between devices.

bean1.clickSomeButton(); //perfoms the action on device 1/ emilator 1
bean2.clickSomeButton(); //perfoms the action on device 2/ emilator 2

I use Spring for pages, so they look like:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Page {
}
...
@Page
public class SomePage {

  @AndroidFindBy(id = PACKAGE + ":id/someButton”)
  private WebElement someButton;

  @When("Press some button")
  public void pressSomeButton() {
    someButton.click();
  }

  @Then("Button is pressed")
  public void verifyButtonIsPressed() {
    Assert.assertTrue(...);
  }

}

I use JBehave for tests, so they look like:

Scenario: This test case presses some button and verifies it is pressed.

When Press some button
Then Button is pressed

JBehave knows about pages because of this:

public abstract class AbstractSpringJBehaveStory extends JUnitStory {
  ...
  @Override
  public InjectableStepsFactory stepsFactory() {
    return new SpringStepsFactory(configuration(), applicationContext);
  ...
}

So the workflow can be described as follows:

  1. JBehave scenario is executed with SpringJUnit4ClassRunner
  2. Spring starts scanning all beans
  3. Spring detects Page beans (annotated with @page) and creates ONLY ONE bean for each page
  4. JBehave script is executed step by step, Page beans are detected automatically in the application context

So it is impossible to switch between same page decorated with different drivers. Only a single instance of each page is on the context. This can be "fixed" if, for example, forget about @AndroidFindBy:

public class SomePage {

  private By someButton = By.id(PACKAGE + ":id/someButton”));

  @When("Press some button")
  public void pressSomeButton() {
    driver.findElement(someButton).click(); // driver is used directly and be switched
  }

}

@TikhomirovSergey
Copy link
Contributor

There is no conversations anymore.
It is defenitly the problem of user's framework or used third party lidrary.
Maybe the fix of #228 will simplify this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants