javatestingseleniumabstract-classconcreteclass

Using concrete class from a abstract type variable


I'm sorry if this question has been asked already, I haven't found anything like my question yet...

I'm working/playing/learning to build up some kind of testing environment... Inside it, I'm building an Application Layer (a package of classes that are the virtual representation of the different pages/windows/forms) of an application. The simplified setup is the following:

public abstract class WebPage {

    protected WebDriver driver;

    protected WebElement getElement(By by){
        WebElement element = (new WebDriverWait(driver, 10))
              .until(ExpectedConditions.presenceOfElementLocated(by));

    return element;
}

    public void menuLogout(){
        System.out.println("Logged out");
    }
}

public class HomePage extends WebPage {

    public ProfilePage ClickLinktoProfilePage(){
        return new ProfilePage();
    }

    public DashBoardPage clickViewDashboard(){
        return new DashBoardPage();
    }

    public String getTitle(){
        return getElement(By.id("title")).getText();
    }
}

public class ProfilePage extends WebPage {

    public String getUsername(){
        return getElement(By.id("name")).getText();
    }

    public String getEmail(){
    return getElement(By.id("email")).getText();
}       
    public HomePage clickReturnToHomePage(){
        return new HomePage();
    }

}

public class DashBoardPage extends WebPage {

    public String getcurrentPeriod(){
        return getElement(By.id("name")).getText();

}
}

The idea behind this is that I wish my Test to hold only one current WebPage. I do not wish to create a new variable each time I change page.

I also do not want to be forced to know in advance which page I'm heading into. I want the application Layer to give me the flow of the Application. In the same way that when clicking a link, you are brought to the following page, I wish that when I click a link that brings me to another page, that method tells me what page I'm heading into.

(WebPage abstract class also exposes lots of shared methods between all concrete WebPages)

So my intended use was:

WebPage currentPage = new HomePage();

currentPage = currentPage.ClickLinktoProfilePage(); //currentPage = new ProfilePage();
System.out.println(currentPage.getUsername());
currentPage.menuLogout();

Sadly, this does not work, since the currentPage variable is typed as WebPage, it cannot see any of the concrete classes's methods. I find it logical and odd at the same time because I can ask "currentPage.getClass().getName();" and it'll return "packageName.ConcreteClassName".

For Typecasting to work, I would need to redefine the variable's type... (not sure if it's possible or even good to do).

So I know I can find the name of the class inside the variable, but I'm not sure where to go from there.

Anyone got a solution?


Solution

  • To clarify what Radiodef and I are saying in the comments here:

    What you want is to define WebPage (your abstract API) in such a way that your concrete subclasses don't need to have public methods that aren't a part of that API.

    For example, compare the java.util.List interface in the standard library. There are multiple implementations of this interface (ArrayList and LinkedList are the most well-known ones, but there are many others), but the majority of code that uses List doesn't need to care whether it's actually using an ArrayList or a LinkedList or something else, since all the operations that you need are exposed via the List interface.

    You can do the same thing with your WebPage class. For example, you could define a series of "hooks" for different operations that you can do with a web page:

    public abstract class WebPage {
      // methods that each subclass needs to implement
      protected abstract String renderBodyHtml();
      public abstract String getNameToLinkTo();
    
      // other methods that are common to every page
      public final void serve(
          HttpServletRequest request, HttpServletResponse response) {
         // write the response, using the specific page's body HTML
         response.getWriter().println(renderToBodyHtml());
      }
    }
    

    And then your pages would implement that contract like so:

    // Note: the class doesn't need to be public, since anybody that uses
    // it can just declare their variable as type WebPage
    class Page1 extends WebPage {
      @Override protected String renderBodyHtml() {
        return "<body>Hello world!</body>";
      }
    
      @Override public String getNameToLinkTo() {
         return "Page1";
      }
    }
    

    Then code that wants to work with a WebPage doesn't need to know that it's a Page1 (or any other page):

    public static void printPageName(WebPage webPage) {
      System.out.println(webPage.getNameToLinkTo());
    }
    

    Alternatively, like resueman says, you can just use the Page1, Page2, etc., types directly, using WebPage only for implementation inheritance, not API. This is fine as well -- the correct solution depends on how flexible (and complex) you want your code to be.