Search code examples
javaselenium-webdriverwebdrivermanager-java

Using Reflection When Casting With Abstract Type


I am all for someone recommending a better title for this particular question. I'm also more than open to working to simplify how I describe the problem.

Context: I have an automation setup where I'm allowing the browser to be configured via a properties file. So if someone has "browser=chrome" in that file, then the specific WebDriver instance that should be instantiated is ChromeDriver.

I'm also using WebDriverManager wherein you can download the binaries for particular WebDriver types. So in this case, I only want to download whatever browser driver is in that properties file. So if that's Chrome, I want to use ChromeDriverManager.

The key thing here, of course, is that I have to generalize all this because I don't know what someone is going to use. But for purposes of my question here, and to show the problem, let's stick with these moving parts: "chrome", ChromeDriver, ChromeDriverManager.

Code:

I have a driverMap that holds an instance of a WebDriver class that is associated with a browser name.

private static final Map<String, Class<?>> driverMap = new HashMap<String, Class<?>>() {
    {
        put("chrome", ChromeDriver.class);
        put("firefox", FirefoxDriver.class);
    }
};

I also have a driverManager that associates a BrowserManager class with a particular WebDriver class.

private static final Map<Class<?>, Class<?>> driverManager = new HashMap<Class<?>, Class<?>>() {
    {
        put(ChromeDriver.class, ChromeDriverManager.class);
        put(FirefoxDriver.class, FirefoxDriverManager.class);
    }
};

Just for more context, all of this is in a class called Driver and it starts like this:

public final class Driver {
    private static WebDriver driver;
    private static BrowserManager manager;
   ....
}

Those two variables are relevant here for the next bit. An add method is called to add a particular browser configuration to the tests. So here is that method, which shows how the above are used when a browser is added to the mix:

public static void add(String browser, Capabilities capabilities) throws Exception {
    Class<?> driverClass = driverMap.get(browser);
    Class<?> driverBinary = driverManager.get(driverClass);

    manager = (BrowserManager) driverBinary.getConstructor().newInstance(); /// <<--- PROBLEM

    driver = (WebDriver) driverClass.getConstructor(Capabilities.class).newInstance(capabilities);
}
  • You can see I use driverClass, which will be something like this: org.openqa.selenium.chrome.ChromeDriver.

  • You can see I use driverBinary, which will be something like this: io.github.bonigarcia.wdm.ChromeDriverManager.

But I commented the line above where I have a problem.

Problem: You can see I use a driver variable to store the WebDriver instance and a manager variable to store the BrowserManager instance.

Here's how and why I'm doing that in the case of driver:

So what that does is get me the appropriate type (ChromeDriver) of the more general (WebDriver). Thus on my driver variable, I am able to cast the reflection call to WebDriver and thus reference driver as if it was that instance.

I can't do the same for manager.

And I don't know if that's because of how that particular Java library works. Specifically:

So I can't call methods on manager as if it was a specific type of BrowserManager (like ChromeDriverManager) as I can for driver (which is a specific type of WebDriver, like ChromeDriver).

This would seem to be because ultimately WebDriver is an interface but BrowserManager is abstract.

So I don't know how to achieve the effect I want. Specifically, the effect I want is to make a call equivalent to this:

ChromeDriverManager.getInstance().setup();

But I have to do that using the reflection since I don't know what manager I'll be using. So ideally I want it so that I can do this:

manager.getInstance().setup();

I don't know what I can cast down to in order to make manager work. Or I don't know if I can cast to a specific class once I've determined what that class is.

I can just abandon using WebDriverManager entirely but it is a nice solution and I'm hoping to find some way to do what I need.


Solution

  • So I don't know how to achieve the effect I want. Specifically, the effect I want is to make a call equivalent to this:

    ChromeDriverManager.getInstance().setup();
    

    But I have to do that using the reflection since I don't know what manager I'll be using. So ideally I want it so that I can do this:

    manager.getInstance().setup();
    

    I don't know what I can cast down to in order to make manager work. Or I don't know if I can cast to a specific class once I've determined what that class is.

    Upon investigation, I find that ChromeDriverManager.getInstance() is a static method. Static methods are bound at compile time, not runtime, so you cannot invoke that method via a normal method invocation expression if you don't know at compile time which class's method you want to invoke. And the whole point is that you don't know that.

    But this is silly. The point of that method is to provide an instance of the class, registered with BrowserManager as a designated special instance. It makes no sense to attempt to do that by first obtaining some other instance that you don't need for anything else, because you don't need an instance of a class to invoke the class's static methods, either.

    It appears that the concrete BrowserManager subclasses implement a pattern of such getInstance() methods. Although these are not polymorphic, and therefore are not guaranteed to be present, you may be able rely on the pattern to locate and invoke them reflectively (instead of invoking the constructor reflectively). For example,

        Class<?> driverBinary = driverManager.get(driverClass);
    
        try {
            // Retrieves a no-arg method of the specified name, declared by the
            // driverBinary class
            Method getInstanceMethod = driverBinary.getDeclaredMethod("getInstance");
    
            // Invokes the (assumed static) method reflectively
            BrowserManager manager = (BrowserManager) getInstanceMethod.invoke(null);
    
            manager.setup();
        } catch ( IllegalAccessException
                | IllegalArgumentException
                | InvocationTargetException
                | NoSuchMethodException
                | SecurityException e) {
            // handle exception
        }
    

    You can invoke all of the instance methods declared by BrowserManager on the resulting object. In particular, you can invoke setup(), as shown.

    On the other hand, if you don't need to register your instance as the special designated BrowserManager instance, then you don't need to go through getInstance() at all. The method you already have for obtaining an instance will suffice for getting you an instance, and you could then invoke its setup() method directly. I'm not sure whether not having the instance registered with BrowserManager would present any problem.