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(第一个)。
我试过:
- 使用
IObjectContainer
接口然后在InjectWebDriver
函数中执行containers[featureContext.FeatureInfo.Title].RegisterInstanceAs<IWebDriver>(drivers[featureContext.FeatureInfo.Title])
- 使用
Thread.CurrentThread.ToString()
而不是 featureContext.FeatureInfo.Title
进行索引
- 要在
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 并行化,应使用
NUnit
或 xUnit
作为测试框架。然而 NUnit
的好处是它按字母排序顺序为测试提供开箱即用的排序。如果您希望在同一个功能文件中按顺序有两个场景 运行,这将特别有用。即在每个场景标题前放置一个数字将确保它们在执行期间的顺序。 xUnit
本身不支持此功能,通过四处搜索似乎很难实现该目标。
- Specflow 在并行化方面更“友好”,scenario-level 并行化。这就是为什么
Specflow+ Runner
测试框架由 SpecFlow 运行s 场景级别并行。看起来 SpecFlow 的整个理念(我还不会说 BDD)是拥有独立的场景。当然,这并不意味着您不能通过使用其他测试框架获得非常好的 feature-level 并行化。只是把它放在那里,作为在起草编写功能文件的策略时阅读本文的人的提醒。
我有一个这样的 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(第一个)。
我试过:
- 使用
IObjectContainer
接口然后在InjectWebDriver
函数中执行containers[featureContext.FeatureInfo.Title].RegisterInstanceAs<IWebDriver>(drivers[featureContext.FeatureInfo.Title])
- 使用
Thread.CurrentThread.ToString()
而不是featureContext.FeatureInfo.Title
进行索引 - 要在
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 的
- 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 并行化,应使用
NUnit
或xUnit
作为测试框架。然而NUnit
的好处是它按字母排序顺序为测试提供开箱即用的排序。如果您希望在同一个功能文件中按顺序有两个场景 运行,这将特别有用。即在每个场景标题前放置一个数字将确保它们在执行期间的顺序。xUnit
本身不支持此功能,通过四处搜索似乎很难实现该目标。 - Specflow 在并行化方面更“友好”,scenario-level 并行化。这就是为什么
Specflow+ Runner
测试框架由 SpecFlow 运行s 场景级别并行。看起来 SpecFlow 的整个理念(我还不会说 BDD)是拥有独立的场景。当然,这并不意味着您不能通过使用其他测试框架获得非常好的 feature-level 并行化。只是把它放在那里,作为在起草编写功能文件的策略时阅读本文的人的提醒。
FeatureContext.FeatureInfo.Title
索引的静态字典