SpecFlow、Selenium、NUnit、并行化:来自两个不同 NUnit 测试的 ChromeDriver Windows,保持无法解释的关系

SpecFlow, Selenium, NUnit, Parallelization: ChromeDriver Windows from two different NUnit Tests, keep having unexplained relation

我有一个这样的 selenium-webdriver-di.cs 文件:

using TechTalk.SpecFlow;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium;
using BoDi;
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;

public class WebDriverHooks
{
    private readonly IObjectContainer container;
    private static Dictionary<string, ChromeDriver> drivers = new Dictionary<string, ChromeDriver>();
    private ScenarioContext _scenarioContext;
    private FeatureContext _featureContext;

    public WebDriverHooks(IObjectContainer container, ScenarioContext scenarioContext, FeatureContext featureContext)
    {
        this.container = container;
        _scenarioContext = scenarioContext;
        _featureContext = featureContext;
    }
    [BeforeFeature]
    public static void CreateWebDriver(FeatureContext featureContext)
    {
        Console.WriteLine("BeforeFeature");
        var chromeOptions = new ChromeOptions();
        chromeOptions.AddArguments("--window-size=1920,1080");
        drivers[featureContext.FeatureInfo.Title] = new ChromeDriver(chromeOptions);
    }
    [BeforeScenario]
    public void InjectWebDriver(FeatureContext featureContext)
    {
        if (!featureContext.ContainsKey("driver"))
        {
            featureContext.Add("driver", drivers[featureContext.FeatureInfo.Title]);
        }
    }
    [AfterFeature]
    public static void DeleteWebDriver(FeatureContext featureContext)
    {
        ((IWebDriver)featureContext["driver"]).Close();
        ((IWebDriver)featureContext["driver"]).Quit();
    }

然后在我的每个包含步骤定义的 .cs 文件中,我都有这样的构造函数:

using System;
using TechTalk.SpecFlow;
using NUnit.Framework;
using OpenQA.Selenium;
using System.Collections.Generic;
using PfizerWorld2019.CommunityCreationTestAutomation.SeleniumUtils;
using System.Threading;
using System.IO;

namespace PfizerWorld2019
{
    [Binding]
    public class SharePointListAssets
    {
        private readonly IWebDriver driver;
        public SharePointListAssets(FeatureContext featureContext)
        {
            this.driver = (IWebDriver)featureContext["driver"];
        }
    }
}

然后我在 class 的所有函数中使用了 driver 变量。最后,我有一个名为 Assembly.cs 的文件,我将其用于 NUnit 夹具级并行化:

using NUnit.Framework;
[assembly: Parallelizable(ParallelScope.Fixtures)]

在 SpecFlow 的术语中,这意味着特征级别的并行化(1 个 .feature 文件 = 1 个 Nunit 测试 = 1 个 Nunit Fixture)

如果我 运行 连续测试,它们工作正常。

但是如果我 运行 并行进行 2 个测试,任何两个测试,总是会发生一些有趣的事情。例如:第一个 Chromedriver window 尝试点击一个元素,当且仅当第二个 Chromedriver window(即 运行 进行不同的测试)呈现完全相同的部分时,它才会点击它的网站。但它将点击发送到正确的 window(第一个)。

我试过:

  1. 使用IObjectContainer接口然后在InjectWebDriver函数中执行containers[featureContext.FeatureInfo.Title].RegisterInstanceAs<IWebDriver>(drivers[featureContext.FeatureInfo.Title])
  2. 使用 Thread.CurrentThread.ToString() 而不是 featureContext.FeatureInfo.Title 进行索引
  3. 要在 CreateWebDriver 函数中执行 featureContext.Add(featureContext.FeatureInfo.Title + "driver", new ChromeDriver(chromeOptions) 而不是 drivers[featureContext.FeatureInfo.Title] = new ChromeDriver(chromeOptions);

我只是不明白是什么允许这种“共享”。由于 FeatureContext 用于与驱动程序生成和销毁相关的所有内容,因此两个 chromedriver 如何相互交谈?

更新:我试过这样的驱动程序初始化和共享:

[BeforeFeature]
public static void CreateWebDriver(FeatureContext featureContext)
{
    var chromeOptions = new ChromeOptions();
    chromeOptions.AddArguments("--window-size=1920,1080");
    chromeOptions.AddArguments("--user-data-dir=C:/ChromeProfiles/Profile" + uniqueIndex);
    WebdriverSafeSharing.setWebDriver(TestContext.CurrentContext.WorkerId, new ChromeDriver(chromeOptions));
}

我制作了一个 webdriver-safe-sharing.cs 文件,如下所示:

class WebdriverSafeSharing
{
    private static Dictionary<string, IWebDriver> webdrivers = new Dictionary<string, IWebDriver>();
    public static void setWebDriver(string driver_identification, IWebDriver driver)
    {
        webdrivers[driver_identification] = driver;
    }
    public static IWebDriver getWebDriver(string driver_identification)
    {
        return webdrivers[driver_identification];
    }
}

然后在每个步骤定义函数中,我只是调用 WebdriverSafeSharing.getWebDriver(TestContext.CurrentContext.WorkerId) 而不涉及 FeatureContext。而且我仍然遇到同样的问题。请注意我的做法 chromeOptions.AddArguments("--user-data-dir=C:/ChromeProfiles/Profile" + uniqueIndex); 因为我也开始不相信 chromedriver 本身是线程安全的。但即便如此也无济于事。

更新 2:它尝试了一个更偏执的 webdriver-safe-sharing.cs class:

class WebdriverSafeSharing
{
    private static readonly Dictionary<string, ThreadLocal<IWebDriver>> webdrivers = new Dictionary<string, ThreadLocal<IWebDriver>>();
    private static int port = 7000;
    public static void setWebDriver(string driver_identification)
    {
        lock (webdrivers)
        {
            ChromeDriverService service = ChromeDriverService.CreateDefaultService();
            service.Port = port;
            var chromeOptions = new ChromeOptions();
            chromeOptions.AddArguments("--window-size=1920,1080");
            ThreadLocal<IWebDriver> driver =
            new ThreadLocal<IWebDriver>(() =>
            {
                return new ChromeDriver(service, chromeOptions);
            });
            webdrivers[driver_identification] = driver;
            port += 1;
            Thread.Sleep(1000);
        }
    }
    public static IWebDriver getWebDriver(string driver_identification)
    {
        return webdrivers[driver_identification].Value;
    }

它有一把锁,一个Threadlocal和唯一的端口。它仍然不起作用。完全相同的问题。

更新 3:如果我 运行 两个单独的 Visual Studio 实例并且我 运行 对每个实例进行 1 次测试,它就可以工作。或者通过并排放置两个相同的项目 运行

然后选择 运行 并行测试:

问题似乎是您将 IWebDriver 对象隐藏在 FeatureContext 中。为功能中的每个场景创建和重用 FeatureContext。虽然从表面上看,使用 NUnit running tests in parallel 似乎是安全的(它不会 运行 在同一功能中并行使用场景),但我的直觉是这并不像您想象的那么安全。

相反,使用每个 场景 初始化和销毁​​ IWebDriver 对象,而不是功能。 ScenarioContext 应该是线程安全的,因为它为每个场景创建一次,并且只用于一个场景。我建议改用依赖注入:

[Binding]
public class WebDriverHooks
{
    private readonly IObjectContainer container;

    public WebDriverHooks(IObjectContainer container)
    {
        this.container = container;
    }

    [BeforeScenario]
    public void CreateWebDriver()
    {
        var driver = // Initialize your web driver here

        container.RegisterInstanceAs<IWebDriver>(driver);
    }

    [AfterScenario]
    public void DestroyWebDriver()
    {
        var driver = container.Resolve<IWebDriver>();

        // Take screenshot if you want...

        // Dispose of the web driver
        driver.Dispose();
    }
}

然后将构造函数参数添加到您的步骤定义 类 以传递 IWebDriver:

[Binding]
public class FooSteps
{
    private readonly IWebDriver driver;

    public FooSteps(IWebDriver driver)
    {
        this.driver = driver;
    }

    // step definitions...
}

原因是所有 selenium API 操作都包含在我编写的 static 方法中。这是我几周前为代码 re-usability 编写的文件中的一个 class。然而,由于不习惯在 C# 中进行并行编程,老实说,我不再知道这些方法已声明 static。我现在 运行在 Selenium Grid 上有 20 个并行工作器。

不过,如果遇到 NUnit、SpecFlow 和 Selenium 的并行化问题,我会在此处放置一些重要的注意事项

  • 如果目标是 feature-level 而不是 scenario-level 并行化,则 WebDriver 的初始化必须在 [BeforeFeauture] 绑定方法中完成。
  • WebDriver 的初始化必须是线程安全的。我所做的是我使用了一个由包含 WebDrivers
  • FeatureContext.FeatureInfo.Title 索引的静态字典
  • chromedriver 是线程安全的。不需要唯一的 data-dir 文件夹或唯一的端口或唯一的 chromedriver 文件名。人们可能感兴趣的 --headless--no-sandbox 参数并没有给我带来任何并行化问题(无论是使用 Selenium Grid 还是简单的单一 multi-core 机器并行化)。基本不怪chromedriver.
  • 为了注入 webdriver,请在 [BeforeScenario] 绑定方法中使用 IObjectContainer 接口。很棒很安全。
  • driver.Dispose() 处理 [AfterFeature] 绑定方法中的 driver,这样就没有僵尸进程了。这对我使用 Selenium Grid 很有帮助,因为当我使用 driver.Close()driver.Quit() 时,节点不会在后者完成后终止进程。
  • 对于启用了 [assembly: Parallelizable(ParallelScope.Fixtures)]NUnit.feature 文件中的所有场景 运行 都在同一个 FeatureContext 中,因此场景是 FeatureContext-安全。这意味着您可以信任 FeatureContext 在同一个 .feature 文件中的场景之间共享数据。
  • 每个 .feature 文件只调用一次 [BeforeFeature] 挂钩(正如人们逻辑上假设的那样)。因此,如果您有 x .feature 个文件,则该挂钩将在测试期间被调用 x 次 运行.
  • 正如 Specflow 文档所说,对于 feature-level 并行化,应使用 NUnitxUnit 作为测试框架。然而 NUnit 的好处是它按字母排序顺序为测试提供开箱即用的排序。如果您希望在同一个功能文件中按顺序有两个场景 运行,这将特别有用。即在每个场景标题前放置一个数字将确保它们在执行期间的顺序。 xUnit 本身不支持此功能,通过四处搜索似乎很难实现该目标。
  • Specflow 在并行化方面更“友好”,scenario-level 并行化。这就是为什么 Specflow+ Runner 测试框架由 SpecFlow 运行s 场景级别并行。看起来 SpecFlow 的整个理念(我还不会说 BDD)是拥有独立的场景。当然,这并不意味着您不能通过使用其他测试框架获得非常好的 feature-level 并行化。只是把它放在那里,作为在起草编写功能文件的策略时阅读本文的人的提醒。