Selenium和并行化JUnit - WebDriver实例

3

安装

基本上,我正在尝试使用JUnit实现并行运行的Selenium测试。

为此,我找到了这个JUnit Runner。它的效果非常好,我很喜欢它。

然而,我在处理WebDriver实例时遇到了问题。

我的需求

每个WebDriver元素应在每个类执行 @Test 方法之前创建一次。

从逻辑上讲,我可以使用类的构造函数来做到这一点。实际上,这对我的测试来说相当重要,因为我需要使用@Parameters,以便可以相应地创建WebDriver实例(Chrome、FF、IE...)。

问题

问题在于,我希望WebDriver实例在一个类完成后被清除(driver.quit()),而不是在每个@Test方法完成后被清除。但是我不能使用@AfterClass,因为我不能使WebDriver成为静态成员,因为每个类实例都必须使用自己的WebDriver实例(否则测试将尝试在同一个浏览器中运行)。

可能的解决方案

我在这里找到了一个可能的建议,由Mrunal Gosar提出。按照他的建议,我将WebDriver更改为static ThreadLocal<WebDriver>,然后在每个构造函数中创建它的实例。

 // in the classes constructor

driver = new ThreadLocal<WebDriver>() {
           @Override
           protected WebDriver initialValue() {
                 return new FirefoxDriver(); /
           }
};

毋庸置疑,我在代码中用driver.get().whatever替换了每个driver.whatever调用。
现在,为了解决这个问题,我还编写了一个@AfterClass方法,它将调用driver.get().quit();,这个变量现在被编译器接受,因为它是静态的。
然而,测试结果却出现了意外行为。我有一个运行在远程机器上的Selenium Grid设置,其中有2个节点。我以前已经按预期运行了这个设置,但现在浏览器到处都是,测试失败了。(应该只有2个浏览器在运行,但实际上打开了8个以上)
我链接的线程建议使用这种解决方案的人评论说,如果已经使用JUnit等框架,手动处理线程可能是一个不好的主意。
我的问题是什么?
如何正确设计这个问题?
我只能想到以下几种方法:
1. 让这里建议的方法起作用。 2. 编写一个单一的@Test注释方法来执行所有其他方法,然后使用@After来达到与@AfterClass相同的效果。 3. 将构造函数参数保存在成员变量中,并处理每个@Test注释方法在执行之前必须创建一个浏览器的事实(使用@Before来创建WebDriver实例,使用@After来关闭会话)。
不过,我不太确定第3种方法是否会遇到可能的问题。如果我在每个方法之后关闭会话,那么grid-Server可能会在这个节点上打开一个全新的类的新会话,而这个节点上的先前类还没有完成。虽然测试彼此独立,但我仍然觉得这是潜在的危险。
有没有人正在积极使用多线程Selenium测试套件,并能指导我什么是正确的设计?

一个快速的提示:我目前解决方案遇到的问题在于每个类方法似乎都会调用类构造函数。因此,与其拥有x个类实例(其中x是从“@Parameters”函数返回的列表中的参数数量),我实际上拥有x*y个实例,其中y是类内部带有“@Test”注释的方法的数量。 我不太清楚该怎么办。也许我使用的Runner需要进行一些调整,但我怀疑我能否做得更好。也许这种行为也是必要的,我不知道。 - Mercious
我还没有尝试过另一种涉及ThreadLocal的解决方案。使用这种方法,我将ThreadLocal<WebDriver> sWebDriver声明为静态成员变量,并在那里重写了initialValue。然后,我声明了一个“普通”的WebDriver myWebDriver;也作为成员变量。然后,在@Before中,我执行myWebDriver = sWevDriver.get();并在其余代码中使用myWebDriver。这样做的效果是只打开了2个WebDriver实例,并且测试使用了正确的实例。但是,在@AfterClass中,我定义了sWebDriver.get().quit(),这似乎仅适用于两个实例中的一个。 - Mercious
为确保您想要实现的目标: 1. 编写一个具有T个测试用例的类(我们称之为T1,T2等)。 2. 使用JUnit参数化(P浏览器类型,我们称之为P1,P2等)让JUnit运行PxT个测试用例。 3. 对于特定的PxT1、PxT2、PxT3等P类型的测试用例集,仅实例化特定的Px WebDriver一次,并在结束时销毁它。 4. 并行执行测试用例,但仅在浏览器类型上(换句话说,保证不会同时运行具有相同Px的测试)。 / 是这样吗? - Tomasz Domzal
理论上来说,可以并行执行更多的浏览器实例。因此,我不需要保证浏览器只运行一次。然而,你提出了一个有趣的观点,老实说,同时运行3个类可能已经足够了(实际上是一个带有3个不同参数(浏览器)的类)。这将提高一些速度并使事情更加有效。但问题仍然存在。对于这3个并行运行的类的实例,我如何实例化我的webdriver,以便我仍然可以在@AfterClass中清除它? - Mercious
1个回答

5

总的来说,我同意:

如果已经使用像JUnit这样的框架,则手动处理线程可能是一个不好的主意。

但是,看着你提到的Parallelized运行器和JUnit 4.12中@Parametrized的内部实现,这是可能的。

每个测试用例都被安排执行。默认情况下,JUnit在单个线程中执行测试用例。 Parallelized扩展了Parametrized,以使单线程测试调度程序替换为多线程调度程序,因此,要了解这如何影响运行Parametrized测试用例的方式,我们必须查看JUnit Parametrized源代码:

https://github.com/junit-team/junit/blob/r4.12/src/main/java/org/junit/runners/Parameterized.java#L303

看起来像:

  1. @Parametrized 测试用例被拆分成一组 TestWithParameters,每个测试参数一个
  2. 为每个 TestWithParameters 实例创建并安排执行的 Runner(在这种情况下,Runner 实例是专门的 BlockJUnit4ClassRunnerWithParameters

实际上,每个 @Parametrized 测试用例都会生成一组要运行的测试实例(每个参数一个实例),每个实例都独立地安排执行,因此在我们的情况下(使用 WebDriver 实例作为参数的 Parallelized@Parametrized 测试),多个独立的测试将在每个 WebDriver 类型的专用线程中执行。这很重要,因为它允许我们在当前线程的范围内存储特定的 WebDriver 实例。

记住,这种行为依赖于 junit 4.12 的内部实现细节,可能会发生变化(例如,请参见 RunnerScheduler 中的注释)。

请看下面的示例。它依赖于提到的JUnit行为,并使用ThreadLocal存储在同一测试用例组中共享的WebDriver实例。唯一的技巧是仅在@Before中初始化ThreadLocal,并销毁每个创建的实例(在@AfterClass中)。
package example.junit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

/**
 * Parallel Selenium WebDriver example for https://dev59.com/sYvda4cB1Zd3GeqPUhJo
 * Parallelized class is like http://hwellmann.blogspot.de/2009/12/running-parameterized-junit-tests-in.html
 */
@RunWith(Parallelized.class)
public class ParallelSeleniumTest {

    /** Available driver types */
    enum WebDriverType {
        CHROME,
        FIREFOX
    }

    /** Create WebDriver instances for specified type */
    static class WebDriverFactory {
        static WebDriver create(WebDriverType type) {
            WebDriver driver;
            switch (type) {
            case FIREFOX:
                driver = new FirefoxDriver();
                break;
            case CHROME:
                driver = new ChromeDriver();
                break;
            default:
                throw new IllegalStateException();
            }
            log(driver, "created");
            return driver;
        }
    }

    // for description how to user Parametrized
    // see: https://github.com/junit-team/junit/wiki/Parameterized-tests
    @Parameterized.Parameter
    public WebDriverType currentDriverType;

    // test case naming requires junit 4.11
    @Parameterized.Parameters(name= "{0}")
    public static Collection<Object[]> driverTypes() {
        return Arrays.asList(new Object[][] {
                { WebDriverType.CHROME },
                { WebDriverType.FIREFOX }
            });
    }

    private static ThreadLocal<WebDriver> currentDriver = new ThreadLocal<WebDriver>();
    private static List<WebDriver> driversToCleanup = Collections.synchronizedList(new ArrayList<WebDriver>());

    @BeforeClass
    public static void initChromeVariables() {
        System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
    }

    @Before
    public void driverInit() {
        if (currentDriver.get()==null) {
            WebDriver driver = WebDriverFactory.create(currentDriverType);
            driversToCleanup.add(driver);
            currentDriver.set(driver);
        }
    }

    private WebDriver getDriver() {
        return currentDriver.get();
    }

    @Test
    public void searchForChromeDriver() throws InterruptedException {
        openAndSearch(getDriver(), "chromedriver");
    }

    @Test
    public void searchForJunit() throws InterruptedException {
        openAndSearch(getDriver(), "junit");
    }

    @Test
    public void searchForStackoverflow() throws InterruptedException {
        openAndSearch(getDriver(), "stackoverflow");
    }

    private void openAndSearch(WebDriver driver, String phraseToSearch) throws InterruptedException {
        log(driver, "search for: "+phraseToSearch);
        driver.get("http://www.google.com");
        WebElement searchBox = driver.findElement(By.name("q"));
        searchBox.sendKeys(phraseToSearch);
        searchBox.submit();
        Thread.sleep(3000);
    }

    @AfterClass
    public static void driverCleanup() {
        Iterator<WebDriver> iterator = driversToCleanup.iterator();
        while (iterator.hasNext()) {
            WebDriver driver = iterator.next();
            log(driver, "about to quit");
            driver.quit();
            iterator.remove();
        }
    }

    private static void log(WebDriver driver, String message) {
        String driverShortName = StringUtils.substringAfterLast(driver.getClass().getName(), ".");
        System.out.println(String.format("%15s, %15s: %s", Thread.currentThread().getName(), driverShortName, message));
    }

}

它将打开两个浏览器窗口,并在每个窗口中同时执行三个测试用例。
控制台会打印出类似以下的内容:
pool-1-thread-1,    ChromeDriver: created
pool-1-thread-1,    ChromeDriver: search for: stackoverflow
pool-1-thread-2,   FirefoxDriver: created
pool-1-thread-2,   FirefoxDriver: search for: stackoverflow
pool-1-thread-1,    ChromeDriver: search for: junit
pool-1-thread-2,   FirefoxDriver: search for: junit
pool-1-thread-1,    ChromeDriver: search for: chromedriver
pool-1-thread-2,   FirefoxDriver: search for: chromedriver
           main,    ChromeDriver: about to quit
           main,   FirefoxDriver: about to quit

你可以看到,驱动程序是为每个工作线程创建一次并在结束时销毁。
总之,在执行线程的上下文中,我们需要类似于@BeforeParameter@AfterParameter的东西,快速搜索显示这样的想法已经在Junit中注册为问题

非常感谢您的这篇文章,Thomasz!我已经点赞了,但请允许我问一下:为什么我们必须在@Before方法中初始化WebDriver实例,而不是在@BeforeClass方法中呢?想想看,我想我误解了Parallelized的工作方式。我以为对于@Parameters中的每个参数,都会有一个类实例运行其测试。相反,似乎对于每个单独的测试方法,都有一个线程在运行? - Mercious
我认为最终让我感到困惑的是,假设参数列表中有3个参数,那么会有3个类实例。我认为在@BeforeClass中初始化驱动程序一次对于类来说是最有效的方式。我认为为x个@Test方法实例化它们x次将是低效的。但是,如果我对线程实例化正确的话,那么每个类实例仅运行一次测试方法。因此,在@Before@BeforeClass中执行操作没有区别。我希望我理解得没错? - Mercious
@BeforeClass 中的代码仅在主线程中执行一次(即使使用 @Parametrized 时也是如此)。因此,您可以迭代并初始化所有所需的 WebDriver 实例,但不能将它们存储在 ThreadLocal 中以便稍后在工作线程执行上下文中使用。另一方面,在 @AfterClass 中清理是可能且更简单的 - 它只被调用一次,我们可以遍历所有已注册的 WebDriver 实例。 - Tomasz Domzal
哦,这是新的。确实非常有趣。好的,非常感谢!我一定会尝试实现这个。自从这个问题被提出以来,整个项目已经得到了很多改进,现在也使用了WebDriverFactory,所以应该很容易实现你建议的内容。 - Mercious
提取所有并行相关的逻辑到超类中非常容易,看一下这个gist就知道了。 - Tomasz Domzal
是的,那基本上就是我已经在测试中所做的了,后来有人稍微重构了一下驱动器生成。不过,我们仍然可以添加ThreadLocal的东西。尽管这对Selenium来说非常常见,但为此找到合适的设计还是很困难的。由于测试非常依赖I/O,因此多线程会给它们带来巨大的好处。希望如果有人有类似的问题,他会找到这个线程,并看到你精心设计的示例代码! - Mercious

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接