【问题标题】:Selenium and Parallelized JUnit - WebDriver instancesSelenium 和并行化 JUnit - WebDriver 实例
【发布时间】:2015-05-20 15:24:51
【问题描述】:

设置

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

为此我找到了this JUnit runner。效果很好,我很喜欢。

但是,我在处理 WebDriver 实例方面遇到了问题。

我想要什么

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

从逻辑上讲,我可以为此使用类构造函数。实际上这是我测试的要求,因为我需要使用 @Parameters 以便我可以相应地创建 WebDriver 实例(Chrome、FF、IE ...)。

问题

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

一个可能的解决方案

我找到了一个可能的建议 here 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. 将构造函数参数保存在成员变量中并处理在执行每个@Testannotated 方法之前我必须创建浏览器的事实(使用@Before 创建WebDriver 实例和@After 关闭会话)

我不太清楚选项 3 是否会遇到可能的问题。如果我在每个方法之后关闭会话,那么网格服务器实际上可能会在该节点完成之前的会话之前在该节点上打开一个具有全新类的新会话。虽然测试彼此独立,但我仍然觉得这是潜在的危险。

这里有没有人积极使用多线程 Selenium 测试套件并且可以指导我什么是正确的设计?

【问题讨论】:

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

标签: java multithreading selenium junit


【解决方案1】:

总的来说,我同意:

如果已经使用 JUnit 之类的框架

但是,看看你提到的Parallelized runner 和 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 测试用例都会生成一组测试实例来运行(每个参数只有一个实例)并且每个实例都是独立调度的,所以在我们的例子中(Parallelized@Parametrized使用WebDriver 实例作为参数进行测试)将在每个WebDriver 类型的专用线程中执行多个独立测试。这很重要,因为它允许我们将特定的WebDriver 实例存储在当前线程的范围内。

记住这种行为依赖于junit 4.12的内部实现细节并且可能会改变(例如参见RunnerScheduler中的cmets)。

让我看看下面的例子。 它依赖于提到的 JUnit 行为并使用 ThreadLocal 来存储在同一案例组中的测试之间共享的 WebDriver 实例。 ThreadLocal 的唯一技巧是只初始化一次(在 @Before 中)并销毁每个创建的实例(在 @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 http://stackoverflow.com/questions/30353996/selenium-and-parallelized-junit-webdriver-instances
 * 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 这样的东西,快速搜索显示这样的想法已经注册为issue in Junit

【讨论】:

  • 非常感谢您的这篇文章,Thomasz!我赞成它,但请允许我问:为什么我们必须在 @Before 方法中初始化 WebDriver 实例,而不是 @BeforeClass 方法?想一想,我想我误解了 Parallelized 的工作方式。我认为对于@Parameters 中的每个参数,都会有一个类实例使用此参数运行它的测试。相反,似乎每一个测试方法都有一个线程在运行?
  • 我认为这最终让我感到困惑。假设 Parameter-List 中有 3 个参数的 3 个类实例,我认为它必须是让驱动程序在 @BeforeClass 中为类初始化 ONCE 的最有效方法。我认为对于 x 数量的 @Test 方法实例化它们 x 次是低效的。但是,如果我对线程实例的看法是正确的,那么每个类实例只针对一个测试方法运行。因此,如果我们执行@Before@BeforeClass,则没有区别。我希望我没听错?
  • @BeforeClass 中的代码仅由 main 线程执行一次(在使用 @Parametrized 时也是如此)。所以是的,您将迭代并初始化所有必需的 WebDriver 实例,但不能将它们存储在 ThreadLocal 中,以便稍后在工作线程执行上下文中可用。另一方面,在@AfterClass 中清理是可能的并且更简单——它只被调用一次,我们可以迭代所有已注册的WebDriver 实例。
  • 哦,那是新的。非常有趣,确实。嗯,非常感谢!我一定会尝试实现这一点。自从这个问题以来,整个事情已经做了很多工作,但它现在也使用了这样的 WebDriverFactory,所以应该很容易实现你的建议。
  • 把所有并行相关的逻辑提取到超类是很容易的,看看这个gist
猜你喜欢
  • 2014-10-24
  • 2015-02-24
  • 2013-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多