0

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.

Jeff Nyman
  • 870
  • 2
  • 12
  • 31
  • You're throwing around a lot of words, but nowhere do you state a specific problem. What's actually wrong with the line you flagged? Does it generate a compile error? (What error?) Does it throw an exception at runtime? (What exception?) – John Bollinger May 12 '17 at 18:31
  • Good point, @JohnBollinger. The problem is with that line that I want to use: `manager.getInstance().setup();` I can't do that because I need `manager` to be an instance of the specific browser binary (`ChromeDriverManager` or `FirefoxDriverManager`). Both of those use `BrowserManager` as their parent, which is abstract. It's hard (for me) to describe this which is why I included the cast example with `WebDriver`). – Jeff Nyman May 12 '17 at 18:37
  • So it's not so much that I get an error as I can't use the code line (`manager.getInstance().setup()`) at all. And that's because I can't apparently cast down to a more generic instance, as I can with WebDriver. – Jeff Nyman May 12 '17 at 18:38
  • I do not understand the problem you claim to have with casting. Given an object O of class C, you can cast any reference to O to any type among C and all of its supertypes, including abstract types. The declared type of the particular reference you are casting does not constrain this, though it may allow the compiler to check casts for plausibility. – John Bollinger May 12 '17 at 18:42
  • @JohnBollinger: The issue is `driver` uses driverClass (`ChromeDriver`) and casts to `WebDriver`. This allows me to call WebDriver methods on `driver`. I can't do the same with `manager`. It uses driverBinary (`ChromeDriverManager`) but ... I don't know what to cast down to. `BrowserManager` compiles, but I can't call methods on `manager` like I can with `driver` -- and I think that's because `BrowserManager` is abstract, while `WebDriver` is an interface. So I don't know what to cast to in the case of `manager` ... or if I should even be casting in that context. – Jeff Nyman May 12 '17 at 18:47

3 Answers3

1

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.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Interesting! Thank you for that information. I'll play around with that. – Jeff Nyman May 12 '17 at 20:32
  • I have to say, I really like this as I try it out. I like that it doesn't require me hardcoding any of the specific driver managers. I also think this answer is entirely in line with the original focus of my question, which might help others out as they explore similar ideas. – Jeff Nyman May 12 '17 at 21:44
0

Going with comments and help by John about whether this approach even makes sense, I did find a slightly brute force way to handle this, which is this:

Class<?> driverBinary = driverManager.get(driverClass);

if (driverBinary.newInstance() instanceof ChromeDriverManager) {
    ChromeDriverManager.getInstance().setup();
}

So here I get rid of the manager variable and just use the driverBinary instance to check if it's an instance of one of the driver managers. Then I can just add a series of else if conditions for each browser. For example:

if (driverBinary.newInstance() instanceof ChromeDriverManager) {
    ChromeDriverManager.getInstance().setup();
} else if (driverBinary.newInstance() instanceof FirefoxDriverManager) {
    FirefoxDriverManager.getInstance().setup();
} else if (...) {
    ...
}

I say "brute force" because I realize this solution does not provide a great deal of finesse. I need to play around with John's provided solution as well.

These kinds of challenges come up a lot in testing frameworks, where you can't know what the conditions are under which the tests are going to be operating. So being able to frame better or worse ways of doing these things does seem useful.

The above is currently shown in my Driver class.

Jeff Nyman
  • 870
  • 2
  • 12
  • 31
  • It is similarly silly to obtain an instance of the `driverBinary` class just to test what its class is. If you want to pursue a course along these lines, then a better condition would be `if (driverBinary == ChromeDriverManager.class)` without using `driverBinary.newInstance()`. Of course, this requires you to hardcode a case for every `BrowserManager` class you want to support. – John Bollinger May 12 '17 at 21:09
  • Fair point and good to know. As a note, you generally come off a lot better if you don't refer to things people try as "silly." There are generally better ways to comment upon things. Nevertheless your assistance has been appreciated and has made me think more about how I'm approaching this. – Jeff Nyman May 12 '17 at 21:35
0

WebDriverManager has different driverManagers for different browsers, i.e. ChromeDriverManager for Chrome, FirefoxDriverManager for Firefox, and so on. Moreover, it has a generic driverManager that can be parameterized. This driver is named directly WebDriverManager. The method getInstance() of driver accepts the WebDriver class for the underlying browser to be used (i.e. ChromeDriver, FirefoxDriver, etc):

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

import io.github.bonigarcia.wdm.WebDriverManager;

// ...

Class<? extends WebDriver> driverClass = ChromeDriver.class;

// ... other option:
// driverClass = FirefoxDriver.class;

WebDriverManager.getInstance(driverClass).setup();
WebDriver driver = driverClass.newInstance();

Here you can find a working example (a JUnit 4 parameterized test to use Chrome and Firefox with the same test logic, in which the generic driverManager resolves the proper binary for Chrome and Firefox)

Boni García
  • 4,618
  • 5
  • 28
  • 44