Why is a pagefactory class returning null when initialised from another class

450 views Asked by At

In my test class, I have DesiredCapabilities set up for Appium test. In that class, I initialised my BasePage class holding pagefactory elements. When I run the test, it works as expected.

Now, I tried to be a bit more creative by moving my DediredCapabilities into a separate class, CapacityManager. In my test class, I called the method holding the DesiredCapabilities from the CapacityManager. The method call was successful, my app was launched, but the pagefactory elements were no longer working. I am not sure why.

When I run the test, I get a nullPointerExceptio on the first mobile element and this makes me suspect that a pagefactory or driver initialisation problem.

The working test class looks like this:

public class LoginTest {
    
      private final BaseUtil baseUtil = new BaseUtil();
      private static BasePage basePage;
      public static AndroidDriver<AndroidElement> driver;
      public static File classpathRoot;
    
    
      public void startApp() throws MalformedURLException {
        classpathRoot = new File(System.getProperty("user.dir"));
        File appDir = new File(classpathRoot, "");
        File app = new File(appDir, baseUtil.getMyApp());
        DesiredCapabilities cap = new DesiredCapabilities();
        cap.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
        cap.setCapability(MobileCapabilityType.PLATFORM_VERSION, "11.0");
        cap.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator");
        cap.setCapability(MobileCapabilityType.AUTOMATION_NAME, "uiautomator2");
        cap.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
        URL url = new URL("http://localhost:4723/wd/hub");
        driver = new AndroidDriver<>(url, cap);
        basePage = PageFactory.initElements(driver, BasePage.class); //pagefactory initialised
      }
    
    
    
      public void login(String sheetname, int rowNumber) throws InterruptedException, IOException, InvalidFormatException {
        FeatureUtils featureUtils = new FeatureUtils();
        File excelDir = new File(classpathRoot, "");
        File exceldoc = new File(excelDir, baseUtil.getUsersExcelDoc());
        List<Map<String, String>> testData = featureUtils.getData(exceldoc.getAbsolutePath(), sheetname);
        String username = testData.get(rowNumber).get("username");
        String password = testData.get(rowNumber).get("password");
  
        basePage.yesIAgreeButton.click(); //first element clicks successfully

This is the BasePage class holding the pagefactory elements:

    public class BasePage {
    
      private final WebDriver driver;

      public BasePage(WebDriver driver) { //constructor
        this.driver = driver;
      }
    
     
      @FindBy(id = "com.test")
  public WebElement yesIAgreeButton;

With the two classes above, the LoginTest class works as expected.

Now, I removed the DesiredCapabilities from the test class and put them in a new class, CapacityManager:

public class CapacityManager {

  public static AndroidDriver<AndroidElement> driver;
  public static File classpathRoot;
  private final BaseUtil baseUtil = new BaseUtil();
  private static BasePage basePage;

  public DesiredCapabilities appDesiredCapabilities() throws MalformedURLException {
    DesiredCapabilities desiredCapabilities = null;
    classpathRoot = new File(System.getProperty("user.dir"));
    File appDir = new File(classpathRoot, "");
    File app = new File(appDir, baseUtil.getMyApp());
    DesiredCapabilities cap = new DesiredCapabilities();
    cap.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
    cap.setCapability(MobileCapabilityType.PLATFORM_VERSION, "11.0");
    cap.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator");
    cap.setCapability(MobileCapabilityType.AUTOMATION_NAME, "uiautomator2");
    cap.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
    URL url = new URL("http://localhost:4723/wd/hub");
    driver = new AndroidDriver<>(url, cap);
    basePage = PageFactory.initElements(driver, BasePage.class); //pagefactory initialisation

    return desiredCapabilities;
  }
   
}
    

I then called the DesiredCapabilities from the test class, like this:

 public class LoginTest {
    
      private final BaseUtil baseUtil = new BaseUtil();
      private static BasePage basePage;
      public static AndroidDriver<AndroidElement> driver;
      public static File classpathRoot;
      CapacityManager capacityManager = new CapacityManager();
    
     
      public void startApp() throws MalformedURLException {
    capacityManager.appDesiredCapabilities();  //method call. App launches successfully.
    }
    
      
    
      public void login(String sheetname, int rowNumber) throws InterruptedException, IOException, InvalidFormatException {
        FeatureUtils featureUtils = new FeatureUtils();
        File excelDir = new File(classpathRoot, "");
        File exceldoc = new File(excelDir, baseUtil.getUsersExcelDoc());
        List<Map<String, String>> testData = featureUtils.getData(exceldoc.getAbsolutePath(), sheetname);
        String username = testData.get(rowNumber).get("username");
        String password = testData.get(rowNumber).get("password");

       basePage.yesIAgreeButton.click(); //getting nullpointer error on this line. Looks like an issue with the pagefactory elements initialisation. basePage is null.
       driver.pressKey(new KeyEvent(AndroidKey.TAB)); 
2

There are 2 answers

0
StrikerVillain On

Objects represent real world entities. For example (taking a page out of the Head First Java reference), a cat is an object. A cat has legs and mouth as features which can be used for doing certain things like running and biting.

Likewise when you created the CapacityManager class and called new operator on it in the LoginTest, you have created an object. CapacityManager has driver, and BasePage (among other features) using which you have initialized in the appDesiredCapabilities() method.

In the case of the Cat, we cannot use the cat's legs to run and use the cat's mouth to bite (there is obviously a way to do this in java but we are getting ahead of ourselves at this point). We can tell the cat to run and bite but in the end the cat has to do these operations by itself. Likewise, CapacityManager features like driver and BasePage cannot be used by us from outside the CapacityManager. We can call the CapacityManager.appDesiredCapabilities() method to initialize the driver, and set page factory on BasePage but we cannot use them directly.

Simply put, declaring driver and BasePage in the LoginTest and creating an object of CapacityManager does not initialize the driver and BasePage features in the LoginTest. We have to figure out how to control the driver and BasePage declared in the CapacityManager in LoginTest. For this we use public access modifier in the CapacityManager for driver and BasePage. The correct OOPs way is to use getters to make the features public so that we can get access to them from outside. This is shown in the below code update.

public class CapacityManager {

    private DesiredCapabilities cap;
    private AndroidDriver<AndroidElement> driver;
    private BasePage basePage;

    public CapacityManager() {
        URL url = new URL("http://localhost:4723/wd/hub");
        this.cap = new DesiredCapabilities();
        cap.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
        cap.setCapability(MobileCapabilityType.PLATFORM_VERSION, "11.0");
        cap.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator");
        cap.setCapability(MobileCapabilityType.AUTOMATION_NAME, "uiautomator2");
        cap.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
        this.driver = new AndroidDriver<>(url, cap);
        this.basePage = PageFactory.initElements(driver, BasePage.class);
    }

    public AndroidDriver<AndroidElement> getDriver() {
        return driver;
    }

    public BasePage getBasePage() {
        return basePage;
    }

}


public class LoginTest {

    private File classpathRoot;
    private BaseUtil baseUtil;
    private File app;

    // Launch the app, initialize driver and page factory
    CapacityManager capacityManager = new CapacityManager();

    public void login(String sheetname, int rowNumber)
            throws InterruptedException, IOException, InvalidFormatException {
        // Intialize file objects
        baseUtil = new BaseUtil();
        classpathRoot = new File(System.getProperty("user.dir"));
        app = new File(new File(classpathRoot, ""), baseUtil.getMyApp());

        FeatureUtils featureUtils = new FeatureUtils();
        File excelDir = new File(capacityManager.classpathRoot, "");
        File exceldoc = new File(excelDir, baseUtil.getUsersExcelDoc());
        List<Map<String, String>> testData = featureUtils.getData(exceldoc.getAbsolutePath(), sheetname);
        String username = testData.get(rowNumber).get("username");
        String password = testData.get(rowNumber).get("password");

        // Get driver and BasePage from capacityManager object
        capacityManager.getBasePage().yesIAgreeButton.click(); 
        capacityManager.getDriver().pressKey(new KeyEvent(AndroidKey.TAB));
    }
}

There are a lot of different and better ways to configure this. If you are feeling even more fancy, you can create a class for getting the testdata. That is for another day.

Hopefully the solution above will solve the error you are getting!

7
hfontanez On

The basePage inside of CapacityManager and the one inside LoginTest are two different objects instances and the one inside LoginTest was never initialized. All you need to do is something like this:

basePage = capacityManager.getBasePage(); // create this method returning basePage object
basePage.yesIAgreeButton.click();

Or... since basePage is static, make it public and call it statically CapacityManager.basePage. That said, I don't think any of those static fields should be static.

UPDATE

The class CapacityManager is poorly constructed. For starters, it has private static fields that are never used for anything after being initialized. I won't fix everything, but I would fix what pertains to this post. Replace CapacityManager with my version and update to fix the issue with the other fields.

public class CapacityManager {

  public AndroidDriver<AndroidElement> driver;
  public File classpathRoot;
  private final BaseUtil baseUtil = new BaseUtil();
  private BasePage basePage;
  private DesiredCapabilities cap;
  private File app;

  public CapacityManager() {
    classpathRoot = new File(System.getProperty("user.dir"));
    File appDir = new File(classpathRoot, "");
    app = new File(appDir, baseUtil.getMyApp());
  }

  public DesiredCapabilities appDesiredCapabilities() throws MalformedURLException {
    if (cap == null) {
        cap = new DesiredCapabilities();
        cap.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
        cap.setCapability(MobileCapabilityType.PLATFORM_VERSION, "11.0");
        cap.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator");
        cap.setCapability(MobileCapabilityType.AUTOMATION_NAME, "uiautomator2");
        cap.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
        URL url = new URL("http://localhost:4723/wd/hub");
        driver = new AndroidDriver<>(url, cap);
    }

    return cap;
  }

  public BasePage getBasePage() {
      return basePage;
  }
}

Without changing anything else in the LoginTest class (I assume it works "correctly"), simply get the BasePage by calling the getter method:

public void login(String sheetname, int rowNumber) throws InterruptedException, IOException, InvalidFormatException {
    FeatureUtils featureUtils = new FeatureUtils();
    File excelDir = new File(classpathRoot, "");
    File exceldoc = new File(excelDir, baseUtil.getUsersExcelDoc());
    List<Map<String, String>> testData = featureUtils.getData(exceldoc.getAbsolutePath(), sheetname);
    String username = testData.get(rowNumber).get("username");
    String password = testData.get(rowNumber).get("password");

    BasePage basePage = PageFactory.initElements(capacityDriver.driver, BasePage.class); // pass the AndroidDriver instance in CapacityDriver class
    basePage.clickYesIAgree();
}

UPDATE #2

Typically, when you create a factory to provide instances of a class, you hide the constructor of that class. When you have public constructors of a class, sometimes is better not to have a factory. Why? To avoid confusion. Of course, there are plenty of exceptions to what I just mentioned. Here, we have one of those confusing implementations.

You have a BasePage with a public constructor that takes a WebDriver as an argument. You also have PageFactory.initElements(driver, BasePage.class) that returns an instance of BasePage. How should I create an instance of BasePage? When should the factory be used? When should I call the contructor directly?

Considering that PageFactory is a proven class provided by the Selenium library, it is reasonable to believe that BasePage is ill contructed and/or the use of the @FindBy annotation is incorrect. None of those I can fix for you, but I can suggest what I believe BasePage should look like. Here you go:

public class BasePage {
    @FindBy(id = "com.test") // this component ID looks suspicious
    private WebElement yesIAgreeButton;

    public void clickYesIAgree() {
        yesIAgreeButton.click();
    }
}

The previous code has been updated after this second update.