【问题标题】:SpecFlow, Selenium, NUnit, Parallelization: ChromeDriver Windows from two different NUnit Tests, keep having unexplained relationSpecFlow、Selenium、NUnit、Parallelization:来自两个不同 NUnit 测试的 ChromeDriver Windows,一直存在无法解释的关系
【发布时间】:2021-06-08 15:59:56
【问题描述】:

我有一个像这样的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"];
        }
    }
}

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

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

这在 SpecFlow 的术语中意味着功能级别的并行化(1 .feature 文件 = 1 Nunit 测试 = 1 Nunit 夹具)

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

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

我试过了:

  1. 使用IObjectContainer接口,然后在InjectWebDriver函数中做containers[featureContext.FeatureInfo.Title].RegisterInstanceAs&lt;IWebDriver&gt;(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 用于所有与驱动程序生成和销毁相关的事情,那么两个 chromedrivers 如何相互通信?

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

[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 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 个测试,它就可以工作。或者让两个相同的项目并排运行

然后选择并行运行测试:

【问题讨论】:

  • 有点困惑:“1 .feature 文件 = 1 Nunit 测试 = 1 Nunit 夹具。” NUnit 测试与 NUnit 夹具不同。有一个文档参考来描述 SpecFlow 如何将功能和场景映射到 NUnit 实体会有所帮助。
  • @charlie 我在 specflow 文档中找不到任何此类文档。您对 1 Nunit 夹具!= 1 Nunit 测试的评论让我感到困扰。如果 1 个 Nunit 测试包含超过 1 个固定装置,那对我来说不是问题吗?但是,Specflow 自动生成的代码为每个特征文件创建 1 个夹具,然后 Nunit 将每个特征文件视为测试,这就是我制作这个 3ple 方程的原因
  • NUnit 中的测试分类...
  • NUnit 使用术语测试来表示一种测试,简单的或复合的。简单(单个)测试是测试用例,由方法和可选参数表示。有各种各样的套件,其中之一是 TestFixture,表示为一个类的单个实例,可以使用或不使用构造函数参数创建。您的示例 TestFixture 包含两个 TestCases。我猜您对“测试”一词的使用在这种情况下只是意味着其他东西,所以我们可能都是“正确的”,但是在处理 NUnit 时,了解 NUnit 术语是件好事。跨度>
  • 归根结底是你最终通过 NUnit 框架运行 NUnit 测试,它完全不了解特性、场景、特性上下文等。可惜没有公共文档SpecFlow 如何将其自己的概念转换为 NUnit,以便更广泛的人(比如我!)实际上可以帮助解决此类问题。

标签: c# selenium parallel-processing nunit specflow


【解决方案1】:

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

相反,使用每个场景而不是功能来初始化和销毁​​ 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...
}

【讨论】:

  • 如果 FeatureContext 不是线程安全的,那就有问题了。但是从 ScenarioContext 重新启动驱动程序对我没有多大帮助,它会延迟功能的执行,而且我必须在许多场景的代码中手动添加它。我可能只是试一试来证明一个替代方案,但我也一直想知道我是否可以在功能级别上做一些事情,这样我就不会遇到问题
  • @Tasos:您在解决方案中使用的字典也不是线程安全的。
  • 你的意思是因为它是静态的?我正在做的索引不能解决这个问题吗? (通过特征上下文标题或线程名称)
  • 我尝试不使用字典,只使用上下文,顺便说一句,我得到了相同的结果(问题中的要点 3)
  • 通过场景上下文进行的驱动程序管理也引入了场景大纲的问题。我不想在每个场景大纲中重新启动驱动程序
【解决方案2】:

原因是所有 selenium API 操作都包含在我编写的 static 方法中。这是我几周前为了代码可重用性而编写的文件中的一个类。然而,我并没有习惯在 C# 中使用并行编程,老实说,我不再知道这些方法被声明为 static。我现在在 Selenium Grid 上运行 20 个并行工作程序。

但是,如果有人遇到 NUnit、SpecFlow 和 Selenium 的并行化问题,我在这里要注意一些重要的注意事项

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

【讨论】:

  • 不错的总结。请注意,NUnit 测试用例的字母顺序几乎是一个无意的“功能”,它可能会消失。最好使用 Order 属性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-11
  • 1970-01-01
  • 2010-12-12
  • 1970-01-01
  • 1970-01-01
  • 2019-12-29
  • 1970-01-01
相关资源
最近更新 更多