使用抽象类型进行转换时使用反射

Using Reflection When Casting With Abstract Type

我完全赞成有人为这个特定问题推荐一个更好的标题。我也非常愿意努力简化我描述问题的方式。

上下文: 我有一个自动化设置,我允许通过属性文件配置浏览器。因此,如果某人在该文件中有 "browser=chrome",那么应该实例化的特定 WebDriver 实例是 ChromeDriver.

我也在使用 WebDriverManager,您可以在其中下载特定 WebDriver 类型的二进制文件。所以在这种情况下,我只想下载该属性文件中的任何浏览器 driver。所以如果那是 Chrome,我想使用 ChromeDriverManager

当然,这里的关键是我必须概括所有这些,因为我不知道有人会使用什么。但是为了我的问题的目的,为了展示问题,让我们坚持使用这些移动部分:"chrome"、ChromeDriverChromeDriverManager.

代码:

我有一个 driverMap,其中包含一个与浏览器名称关联的 WebDriver class 的实例。

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

我还有一个 driverManagerBrowserManager class 与特定的 WebDriver class.

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

只是为了了解更多上下文,所有这些都在一个名为 Driver 的 class 中,它是这样开始的:

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

这两个变量与下一位相关。调用 add 方法将特定的浏览器配置添加到测试中。所以这是该方法,它显示了在将浏览器添加到组合中时如何使用上述方法:

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);
}

但是我评论了上面那行我有问题的地方。

问题: 你可以看到我使用一个 driver 变量来存储 WebDriver 实例和一个 manager 变量来存储 BrowserManager实例.

以下是我在 driver 的情况下这样做的方式和原因:

所以这样做是让我得到更通用 (WebDriver) 的适当类型 (ChromeDriver)。因此,在我的 driver 变量上,我能够将反射调用转换为 WebDriver 并因此引用 driver 就好像它是那个实例一样。

我不能为 manager 做同样的事情。

我不知道这是不是因为那个特定的 Java 库的工作原理。具体来说:

所以我不能调用 manager 上的方法,就像它是 BrowserManager 的特定类型(如 ChromeDriverManager)一样,我可以调用 driver(它是 WebDriver 的特定类型,例如 ChromeDriver).

这似乎是因为最终 WebDriver 是一个接口,而 BrowserManager 是抽象的。

所以不知道怎么才能达到我想要的效果。具体来说,我想要的效果是调用等价于此:

ChromeDriverManager.getInstance().setup();

但我必须使用反射来做到这一点,因为我不知道我将使用哪个管理器。所以理想情况下我想要它,这样我就可以做到这一点:

manager.getInstance().setup();

我不知道为了让 manager 工作我可以做些什么。或者我不知道我是否可以在确定 class 是什么后转换为特定的 class。

我可以完全放弃使用 WebDriverManager,但这是一个很好的解决方案,我希望找到一些方法来完成我需要的事情。

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.

经过调查,我发现 ChromeDriverManager.getInstance() 是一个静态方法。静态方法在编译时绑定,而不是运行时,因此如果您在编译时不知道要调用 class 的哪个方法,则不能通过普通方法调用表达式调用该方法。关键是你不知道。

但这很愚蠢。该方法的要点是提供 class 的实例,并在 BrowserManager 中注册为指定的特殊实例。尝试通过首先获取其他任何您不需要的其他实例来尝试这样做是没有意义的,因为您不需要 class 的实例来调用 class 的静态方法。

似乎具体的 BrowserManager subclasses 实现了这种 getInstance() 方法的模式。尽管它们不是多态的,因此 保证 存在,但您可以依赖模式以反射方式定位和调用它们(而不是反射方式调用构造函数)。例如,

    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
    }

您可以在结果对象上调用 BrowserManager 声明的所有实例方法。特别是,您可以调用 setup(),如图所示。

另一方面,如果您不需要将您的实例注册为特殊指定的BrowserManager实例,那么您根本不需要通过getInstance()。您已有的用于获取实例的方法足以为您获取一个实例,然后您可以直接调用它的 setup() 方法。我不确定不使用 BrowserManager 注册实例是否会出现任何问题。

在约翰的评论和帮助下,关于这种方法是否有意义,我确实找到了一种稍微蛮力的方法来处理这个问题,就是这样:

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

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

所以在这里我摆脱了 manager 变量,只使用 driverBinary 实例来检查它是否是其中一个驱动程序管理器的实例。然后我可以为每个浏览器添加一系列 else if 条件。例如:

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

我说 "brute force" 是因为我意识到这个解决方案并没有提供很多技巧。我也需要尝试一下 John 提供的解决方案。

这类挑战在测试框架中经常出现,您无法知道测试将在什么条件下运行。因此,能够构建更好或更差的做这些事情的方法似乎很有用。

以上目前显示在我的Driverclass.

WebDriverManager 对于不同的浏览器有不同的 driverManagers,即 ChromeDriverManager 对于 Chrome,FirefoxDriverManager 对于 Firefox,等等。此外,它还有一个通用的 driverManager 可以参数化。这个驱动直接命名为WebDriverManager。驱动程序的方法 getInstance() 接受要使用的底层浏览器的 WebDriver class(即 ChromeDriverFirefoxDriver 等):

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 你可以找到一个工作示例(一个 JUnit 4 参数化测试,使用 Chrome 和具有相同测试逻辑的 Firefox,其中通用 driverManager 为 Chrome 和 Firefox 解析正确的二进制文件)