基于.NET Core与Selenium的跨平台UI自动化测试框架实战

📅 2026/6/22 2:44:04
基于.NET Core与Selenium的跨平台UI自动化测试框架实战
1. 项目概述跨平台自动化测试的“瑞士军刀”最近在重构一个老项目的测试流程发现手动点点点不仅效率低下还容易遗漏回归测试点。于是我决定把UI自动化测试给搞起来。项目本身是.NET技术栈但我们的CI/CD流水线既有Windows Server也有Ubuntu的构建节点这就要求自动化测试脚本必须能无缝运行在Windows和Linux上。Selenium WebDriver是UI自动化的老牌选择而.NET Core的跨平台特性正好能完美匹配这个需求。这听起来像是把大象装进冰箱分三步装驱动、写代码、跑起来。但实际做下来从环境配置的坑到跨平台执行的差异每一步都有不少细节需要打磨。这篇文章我就把这次从零搭建一套能在Windows和Linux/Ubuntu双系统下稳定运行的.NET Core Selenium WebDriver自动化测试框架的完整过程、核心原理和踩过的坑毫无保留地分享给你。无论你是刚接触自动化测试的.NET开发者还是正在为多环境部署测试脚本而头疼的测试工程师这套方案都能给你提供一个清晰、可落地的参考。2. 环境准备与核心组件选型2.1 操作系统与.NET Core SDK基础跨平台的第一步是确保你的开发环境和目标运行环境都具备正确的基础。对于Windows我们通常选择Windows 10或Windows Server 2016及以上版本。Linux方面Ubuntu 20.04 LTS或22.04 LTS是经过广泛验证的稳定选择社区支持也最完善。.NET Core SDK的安装是关键。你需要根据目标框架版本进行选择。例如如果你的项目目标是.NET 6长期支持版本那么就需要安装对应的.NET 6 SDK。在Windows上直接从官网下载安装程序即可。在Ubuntu上则通过APT包管理器来安装。这里有个细节务必通过微软官方提供的APT源进行安装而不是使用Ubuntu默认的、可能版本陈旧的源。以下是Ubuntu 22.04上安装.NET 6 SDK的命令# 1. 安装必要的依赖包 sudo apt-get update sudo apt-get install -y wget # 2. 添加微软包仓库和GPG密钥 wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb rm packages-microsoft-prod.deb # 3. 安装.NET SDK sudo apt-get update sudo apt-get install -y dotnet-sdk-6.0安装完成后在终端运行dotnet --info确认SDK版本和运行时信息正确显示。这一步看似简单但统一开发、测试、生产环境的.NET版本能避免大量因运行时差异导致的诡异问题。2.2 浏览器与WebDriver驱动管理Selenium WebDriver本身是一个与浏览器通信的协议它需要通过一个特定的“驱动程序”Driver来操控具体的浏览器。不同的浏览器需要不同的驱动。Chrome/Chromium系浏览器这是目前最主流的选择兼容性好社区资源丰富。你需要两个东西浏览器本体在Windows上直接安装在Ubuntu上可以使用sudo apt install google-chrome-stable安装稳定版。ChromeDriver这是关键。它的版本必须与已安装的Chrome浏览器主版本号严格匹配。例如Chrome版本是 114.0.5735.90那么ChromeDriver也必须是114.x.x.x版本。Firefox浏览器另一个流行的选择驱动名为geckodriver。它的版本兼容性要求相对宽松一些但依然建议使用较新的、相互匹配的版本。驱动管理的最佳实践手动下载并配置PATH是一种方式但在自动化脚本和CI/CD中更推荐程序化管理。我们可以使用WebDriverManager这样的第三方库在.NET中对应的NuGet包是WebDriverManager。它能在运行时自动检测系统已安装的浏览器版本并下载匹配的驱动程序极大地简化了环境配置。我们后续会详细展开如何使用。2.3 项目创建与NuGet包引用一切就绪后我们开始创建测试项目。打开终端Windows用PowerShell或CMDLinux用Bash导航到你的工作目录# 创建一个名为“WebUITests”的NUnit测试项目也可以用xUnit根据团队习惯 dotnet new nunit -n WebUITests cd WebUITests接下来通过dotnet add package命令添加必要的NuGet包# Selenium WebDriver 核心库 dotnet add package Selenium.WebDriver # Selenium 对 Chrome 的支持库如果主要用Chrome dotnet add package Selenium.WebDriver.ChromeDriver # 可选WebDriverManager用于自动管理驱动 dotnet add package WebDriverManager # NUnit 测试适配器与断言库项目模板通常已包含检查即可 # dotnet add package NUnit # dotnet add package NUnit3TestAdapter使用Selenium.WebDriver.ChromeDriver包是一种简便方法它会在项目构建时尝试引入一个匹配的ChromeDriver。但在跨平台场景下它的灵活性不如WebDriverManager因为WebDriverManager能动态处理不同操作系统下的驱动下载。因此在本方案中我们将以WebDriverManager作为主要管理工具。3. 核心架构设计与跨平台适配3.1 设计一个可配置的WebDriver工厂直接在每个测试方法里写死创建ChromeDriver或FirefoxDriver的代码是难以维护的更无法适应跨平台需求。我们需要一个工厂模式Factory Pattern来统一创建WebDriver实例。这个工厂的核心职责是根据配置比如来自环境变量或配置文件创建对应浏览器、并配置好跨平台兼容选项的Driver。首先我们定义一个简单的配置模型可以放在appsettings.json中也可以从环境变量读取// appsettings.json { SeleniumSettings: { BrowserName: Chrome, // 或 Firefox Headless: false, // 是否无头模式运行适用于CI服务器 Platform: Linux // 可选用于特殊平台适配通常可自动检测 } }然后创建我们的WebDriverFactory类using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Firefox; using Microsoft.Extensions.Configuration; namespace WebUITests.Core { public class WebDriverFactory { private readonly IConfiguration _config; public WebDriverFactory(IConfiguration config) { _config config; } public IWebDriver CreateDriver() { var browserName _config[SeleniumSettings:BrowserName] ?? Chrome; var isHeadless bool.Parse(_config[SeleniumSettings:Headless] ?? false); IWebDriver driver; switch (browserName.ToLower()) { case firefox: driver CreateFirefoxDriver(isHeadless); break; case chrome: default: driver CreateChromeDriver(isHeadless); break; } // 公共配置隐式等待、窗口最大化等 driver.Manage().Timeouts().ImplicitWait TimeSpan.FromSeconds(10); driver.Manage().Window.Maximize(); return driver; } private IWebDriver CreateChromeDriver(bool isHeadless) { var options new ChromeOptions(); // 跨平台关键点1无头模式参数 if (isHeadless) { options.AddArgument(--headlessnew); // Chrome 112 推荐使用 new } // 跨平台关键点2沙盒与共享内存设置 // 在Linux的Docker或无头环境中可能需要禁用沙盒并设置/dev/shm使用大小 if (isHeadless || System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux)) { options.AddArgument(--no-sandbox); options.AddArgument(--disable-dev-shm-usage); // 避免 /dev/shm 太小导致崩溃 } // 其他通用参数提升稳定性 options.AddArgument(--disable-gpu); options.AddArgument(--disable-extensions); options.AddArgument(--start-maximized); options.AddExcludedArgument(enable-automation); // 避免被检测为自动化工具 options.AddAdditionalOption(useAutomationExtension, false); // 使用WebDriverManager自动解决驱动问题 new WebDriverManager.DriverManager().SetUpDriver(new WebDriverManager.Config.Impl.ChromeConfig()); // 创建驱动实例 return new ChromeDriver(options); } private IWebDriver CreateFirefoxDriver(bool isHeadless) { var options new FirefoxOptions(); if (isHeadless) { options.AddArgument(--headless); } // Firefox在Linux下的特殊配置相对较少 options.AddArgument(--width1920); options.AddArgument(--height1080); // 使用WebDriverManager new WebDriverManager.DriverManager().SetUpDriver(new WebDriverManager.Config.Impl.FirefoxConfig()); return new FirefoxDriver(options); } } }设计要点解析配置驱动将浏览器类型、是否无头等设置外置使得同一套代码可以通过改变配置在本地调试有界面和CI服务器无头上运行。跨平台参数--no-sandbox和--disable-dev-shm-usage是让Chrome在Linux环境下尤其是容器内稳定运行的关键参数缺一不可。RuntimeInformation.IsOSPlatform用于条件判断使代码更智能。WebDriverManagerSetUpDriver方法会自动检查系统已安装的浏览器版本并从官方镜像下载匹配的驱动到缓存目录无需手动管理驱动路径和版本。3.2 集成依赖注入与配置管理为了让测试项目结构更清晰、更易于维护我们可以将.NET Core的依赖注入DI和配置管理引入测试项目。这通常不被传统测试项目重视但它能极大提升大型测试套件的可管理性。修改Program.cs或创建一个启动类来配置服务// 在NUnit项目中我们可以在[SetUpFixture]或通过[OneTimeSetUp]来配置全局服务 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; [SetUpFixture] public class TestSetup { public static IServiceProvider ServiceProvider { get; private set; } [OneTimeSetUp] public void GlobalSetup() { // 构建配置从appsettings.json和环境变量 var config new ConfigurationBuilder() .AddJsonFile(appsettings.json, optional: true) .AddEnvironmentVariables() .Build(); // 创建服务集合 var services new ServiceCollection(); // 注册配置 services.AddSingletonIConfiguration(config); // 注册WebDriver工厂每次请求新实例 services.AddScopedWebDriverFactory(); // 注册IWebDriver其生命周期为Scoped每个测试用例一个 services.AddScopedIWebDriver(sp { var factory sp.GetRequiredServiceWebDriverFactory(); return factory.CreateDriver(); }); // 注册你的页面对象模型Page Objects或其他服务 // services.AddScopedHomePage(); ServiceProvider services.BuildServiceProvider(); } [OneTimeTearDown] public void GlobalTearDown() { // 清理资源如果需要的话 (ServiceProvider as IDisposable)?.Dispose(); } }然后在你的测试类中就可以通过构造函数注入IWebDriver了using NUnit.Framework; using OpenQA.Selenium; namespace WebUITests.Tests { public class SampleTests { private readonly IWebDriver _driver; // 依赖注入 public SampleTests(IWebDriver driver) { _driver driver; } [SetUp] public void Setup() { // 每个测试方法执行前的操作Driver已由DI容器提供 _driver.Navigate().GoToUrl(https://www.example.com); } [Test] public void TestPageTitle() { Assert.That(_driver.Title, Does.Contain(Example)); } [TearDown] public void TearDown() { // 每个测试方法执行后的操作 // 注意Driver的生命周期是Scoped通常在这里不关闭由DI容器在Scope结束时处理。 // 但为了更稳定的清理可以在这里调用Quit。需要与DI生命周期设置配合。 _driver.Quit(); } } }为什么这么做使用DI管理IWebDriver可以确保每个独立的测试用例拥有自己独立的浏览器实例避免测试间的状态污染。同时配置、工厂等服务的集中管理使得后续添加日志、截图服务、更改浏览器配置都变得非常简单。4. 关键代码实现与页面对象模型4.1 基础操作封装与等待策略直接在所有测试方法中使用_driver.FindElement(By.Id(...))会导致代码重复且脆弱。我们需要对常用操作进行封装并实施稳健的等待策略。1. 显式等待封装Selenium的WebDriverWait是处理动态元素加载的利器。我们封装一个辅助方法using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; namespace WebUITests.Core { public static class WebDriverExtensions { public static IWebElement WaitForElement(this IWebDriver driver, By locator, int timeoutInSeconds 30) { var wait new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds)); // 等待元素存在且可见 return wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementIsVisible(locator)); } public static bool WaitForElementToDisappear(this IWebDriver driver, By locator, int timeoutInSeconds 10) { var wait new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds)); return wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.InvisibilityOfElementLocated(locator)); } public static IWebElement WaitForElementToBeClickable(this IWebDriver driver, By locator, int timeoutInSeconds 30) { var wait new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds)); return wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementToBeClickable(locator)); } } }2. 常用操作封装创建一个BasePage类作为所有页面对象的父类。using OpenQA.Selenium; namespace WebUITests.Pages { public abstract class BasePage { protected readonly IWebDriver Driver; protected BasePage(IWebDriver driver) { Driver driver; } // 封装点击包含等待可点击状态 protected void ClickElement(By locator) { Driver.WaitForElementToBeClickable(locator).Click(); } // 封装输入文本包含清空操作 protected void EnterText(By locator, string text) { var element Driver.WaitForElement(locator); element.Clear(); element.SendKeys(text); } // 封装获取文本 protected string GetElementText(By locator) { return Driver.WaitForElement(locator).Text; } // 判断元素是否存在快速检查不等待 protected bool IsElementPresent(By locator) { try { Driver.FindElement(locator); return true; } catch (NoSuchElementException) { return false; } } } }4.2 实现页面对象模型页面对象模型Page Object Model POM是Selenium测试的核心设计模式。它将页面的元素定位和操作封装在一个类中使测试脚本更清晰维护成本更低。假设我们有一个登录页面LoginPage和一个主页HomePage。// LoginPage.cs using OpenQA.Selenium; namespace WebUITests.Pages { public class LoginPage : BasePage { // 元素定位器 private By UsernameInput By.Id(username); private By PasswordInput By.Id(password); private By LoginButton By.CssSelector(button[typesubmit]); private By ErrorMessage By.ClassName(alert-error); public LoginPage(IWebDriver driver) : base(driver) { // 可以添加页面加载验证 Driver.WaitForElement(LoginButton); } // 页面操作/行为方法 public void EnterUsername(string username) { EnterText(UsernameInput, username); } public void EnterPassword(string password) { EnterText(PasswordInput, password); } public HomePage ClickLogin() { ClickElement(LoginButton); // 返回下一个页面对象 return new HomePage(Driver); } public HomePage Login(string username, string password) { EnterUsername(username); EnterPassword(password); return ClickLogin(); } public string GetErrorMessage() { if (IsElementPresent(ErrorMessage)) { return GetElementText(ErrorMessage); } return string.Empty; } } }// HomePage.cs using OpenQA.Selenium; namespace WebUITests.Pages { public class HomePage : BasePage { private By WelcomeMessage By.Id(welcome); private By LogoutLink By.LinkText(Logout); public HomePage(IWebDriver driver) : base(driver) { Driver.WaitForElement(WelcomeMessage); // 验证已成功跳转到主页 } public string GetWelcomeText() { return GetElementText(WelcomeMessage); } public LoginPage ClickLogout() { ClickElement(LogoutLink); return new LoginPage(Driver); } } }4.3 编写清晰的NUnit测试用例现在我们可以编写非常简洁、易读的测试用例了。using NUnit.Framework; using OpenQA.Selenium; using WebUITests.Pages; namespace WebUITests.Tests { [TestFixture] [Parallelizable(ParallelScope.Fixtures)] // 允许测试类并行执行提升速度 public class LoginTests { private IWebDriver _driver; private LoginPage _loginPage; [SetUp] public void Setup() { // 假设Driver已通过DI或其他方式注入到测试类 // 这里为演示直接创建实际项目用DI var factory new WebDriverFactory(/* config */); _driver factory.CreateDriver(); _loginPage new LoginPage(_driver); _driver.Navigate().GoToUrl(https://your-app.com/login); } [Test] [Category(Smoke)] public void Successful_Login_Navigates_To_HomePage() { // Arrange Act var homePage _loginPage.Login(validUser, validPass); // Assert Assert.That(homePage.GetWelcomeText(), Does.Contain(validUser)); } [Test] public void Failed_Login_Shows_Error_Message() { // Act _loginPage.EnterUsername(wrongUser); _loginPage.EnterPassword(wrongPass); _loginPage.ClickLogin(); // 停留在登录页 // Assert var errorMessage _loginPage.GetErrorMessage(); Assert.That(errorMessage, Is.Not.Empty.And.Contains(invalid)); } [TearDown] public void TearDown() { _driver.Quit(); } } }代码风格要点测试方法命名使用MethodUnderTest_Scenario_ExpectedResult的格式清晰表达测试意图。使用断言NUnit的Assert.That语法可读性更强。测试分类使用[Category]属性对测试进行分类便于选择性地运行如冒烟测试Smoke。5. 跨平台执行与持续集成实战5.1 在Windows与Ubuntu本地运行测试在本地开发机上你只需要确保对应系统安装了正确的.NET SDK和浏览器。通过命令行运行测试# 在项目根目录下 dotnet testdotnet test命令会自动发现并执行项目中的所有测试。如果你想运行特定类别的测试可以使用--filter参数dotnet test --filter CategorySmoke跨平台文件路径处理如果你的测试涉及上传文件需要特别注意文件路径。在C#中可以使用Path.Combine和Path.DirectorySeparatorChar来构建跨平台兼容的路径。string baseDirectory AppContext.BaseDirectory; string testDataPath Path.Combine(baseDirectory, TestData, sample.pdf); // 在Windows上可能是C:\...\TestData\sample.pdf // 在Linux上可能是/home/.../TestData/sample.pdf5.2 集成到CI/CD流水线以GitHub Actions为例持续集成是自动化测试价值最大化的地方。以下是一个.github/workflows/dotnet-test.yml的示例它会在每次推送代码时在Windows和Ubuntu两个Runner上并行运行测试。name: .NET Core UI Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test-on-windows: runs-on: windows-latest steps: - uses: actions/checkoutv3 - name: Setup .NET uses: actions/setup-dotnetv3 with: dotnet-version: 6.0.x - name: Install Chrome run: | choco install googlechrome -y - name: Restore dependencies run: dotnet restore - name: Run UI Tests (Windows) run: dotnet test --configuration Release --verbosity normal --filter Category!Slow # 排除耗时长的测试 env: SeleniumSettings__BrowserName: Chrome SeleniumSettings__Headless: true # CI环境通常用无头模式 test-on-ubuntu: runs-on: ubuntu-22.04 steps: - uses: actions/checkoutv3 - name: Setup .NET uses: actions/setup-dotnetv3 with: dotnet-version: 6.0.x - name: Install Chrome and Dependencies run: | sudo apt-get update sudo apt-get install -y wget gnupg wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - echo deb [archamd64] http://dl.google.com/linux/chrome/deb/ stable main | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Restore dependencies run: dotnet restore - name: Run UI Tests (Ubuntu) run: dotnet test --configuration Release --verbosity normal --filter Category!Slow env: SeleniumSettings__BrowserName: Chrome SeleniumSettings__Headless: trueCI配置解析两个Job并行test-on-windows和test-on-ubuntu两个任务独立运行加快反馈速度。环境准备Windows Runner使用Chocolatey (choco)快速安装Chrome。Ubuntu Runner通过添加Google官方源来安装Chrome。环境变量通过env设置SeleniumSettings__Headless为true强制使用无头模式这是服务器环境的标准做法。配置系统如IConfiguration会自动读取这些环境变量覆盖appsettings.json中的设置。测试过滤使用--filter Category!Slow排除标记为Slow的测试用例保证CI流程快速完成。5.3 测试报告与日志记录在CI中清晰的测试报告至关重要。除了dotnet test自带的输出我们可以集成NUnit的XML报告生成器并结合Allure等工具生成更美观的报告。首先添加报告生成器包dotnet add package NUnitXml.TestLogger然后在运行测试时指定日志格式dotnet test --logger:nunit;LogFilePathTestResult.xml在GitHub Actions中可以将这个XML文件作为产物上传供后续分析或展示。对于更详细的调试建议在测试框架中集成一个简单的日志系统如Microsoft.Extensions.Logging将关键操作、元素查找结果、截图信息记录下来。特别是在测试失败时自动截取屏幕和页面源码能极大提升排查效率。我们可以创建一个ITakesScreenshot的扩展方法在TearDown或发生异常时调用public static void TakeScreenshot(this IWebDriver driver, string testName) { if (driver is ITakesScreenshot screenshotDriver) { var screenshot screenshotDriver.GetScreenshot(); var fileName ${testName}_{DateTime.Now:yyyyMMdd_HHmmss}.png; var filePath Path.Combine(TestResults, Screenshots, fileName); Directory.CreateDirectory(Path.GetDirectoryName(filePath)); screenshot.SaveAsFile(filePath, ScreenshotImageFormat.Png); TestContext.WriteLine($Screenshot saved to: {filePath}); } }在测试类的TearDown中调用[TearDown] public void TearDown() { if (TestContext.CurrentContext.Result.Outcome.Status TestStatus.Failed) { _driver.TakeScreenshot(TestContext.CurrentContext.Test.Name); } _driver.Quit(); }6. 常见问题、性能优化与进阶技巧6.1 跨平台环境下的典型问题与排查即使按照上述步骤操作在跨平台运行时仍可能遇到问题。下面是一个快速排查清单问题现象可能原因Windows可能原因Linux/Ubuntu解决方案ChromeDriver版本不匹配Chrome自动更新后驱动未更新同上或通过包管理器安装的Chrome版本与驱动不匹配使用WebDriverManager自动管理。手动检查chrome://version/查看版本去 ChromeDriver官网 下载对应版本。无法启动浏览器/驱动驱动未加入PATH或路径有空格/中文。驱动缺少执行权限。Windows将驱动所在目录加入系统PATH。Linux运行chmod x chromedriver赋予执行权限。浏览器启动后立即崩溃浏览器与驱动版本严重不匹配。缺少--no-sandbox和--disable-dev-shm-usage参数或/dev/shm空间不足。确保使用正确参数创建ChromeOptions。对于Linux Docker可考虑挂载/dev/shm或设置--shm-size。元素找不到NoSuchElementException页面加载慢元素未出现。无头模式下渲染或JS执行差异导致元素属性/结构变化。1.使用显式等待而非ImplicitWait或Thread.Sleep。2. 增加等待超时时间。3. 在无头模式下尝试添加--window-size1920,1080参数确保布局与有头模式一致。脚本执行超时页面JS复杂或网络慢。同上或系统资源CPU/内存不足。1. 调整PageLoadTimeout和ScriptTimeout。2. 优化测试用例减少不必要的页面跳转。3. 升级CI Runner的配置。字体渲染或布局差异不同系统默认字体不同。Linux服务器可能缺少中文字体。1. 对于视觉回归测试需统一测试环境。2. 在Linux Docker镜像中安装所需字体包apt-get install -y fonts-wqy-zenhei。关键心得Linux无头模式是问题高发区。绝大多数在Windows上运行良好一到Linux CI就失败的测试问题都出在浏览器启动参数、资源限制或等待策略上。务必在Linux环境下进行充分的调试可以使用--headlessfalse先以有头模式运行观察浏览器行为再切换到无头模式。6.2 测试稳定性与性能优化等待策略是稳定的基石彻底抛弃Thread.Sleep。混合使用显式等待针对特定条件和隐式等待设置一个全局的查找元素超时。显式等待应作为首选。使用更稳定的定位器优先使用Id、Name其次是CssSelector和XPath。避免使用依赖于页面结构顺序的索引定位如div[3]/span[2]这类定位在UI微调时极易失效。测试数据隔离每个测试用例应该使用独立的数据避免因数据残留导致测试间相互影响。可以利用数据库事务、API清理或在测试前后执行特定的数据准备/清理脚本。并行测试执行NUnit和xUnit都支持并行执行。合理使用[Parallelizable]属性可以大幅缩短测试套件的总运行时间。注意并行执行时必须确保测试用例之间是独立的不能共享IWebDriver实例这就是为什么我们之前用Scoped生命周期。减少不必要的浏览器启动启动浏览器是昂贵的操作。可以考虑使用[OneTimeSetUp]启动一次浏览器在所有测试间共享但需小心状态清理或者使用更轻量的“复用标签页”模式但这增加了测试的复杂性。对于大多数情况每个测试一个独立的浏览器实例是最简单、最稳定的。6.3 进阶技巧容器化与云测平台集成Docker容器化为了获得极致的环境一致性可以将整个测试环境包括.NET运行时、浏览器、驱动和测试代码打包进Docker镜像。这样在任何地方运行这个镜像都能得到完全相同的测试结果。# Dockerfile for UI Tests FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY . . RUN dotnet restore RUN dotnet publish -c Release -o /app/publish FROM mcr.microsoft.com/dotnet/runtime:6.0 AS runtime WORKDIR /app # 安装Chrome基于Debian的镜像 RUN apt-get update apt-get install -y \ wget gnupg \ wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ echo deb [archamd64] http://dl.google.com/linux/chrome/deb/ stable main /etc/apt/sources.list.d/google-chrome.list \ apt-get update apt-get install -y google-chrome-stable \ rm -rf /var/lib/apt/lists/* COPY --frombuild /app/publish . ENTRYPOINT [dotnet, WebUITests.dll]然后在CI中构建并运行这个Docker镜像来执行测试。集成云测平台对于需要覆盖大量浏览器/操作系统组合的测试可以考虑使用Selenium Grid或商业云测平台如Sauce Labs, BrowserStack。你的.NET测试代码几乎无需改动只需要将IWebDriver的创建指向Grid Hub或云平台的远程地址即可。// 连接远程Selenium Grid var options new ChromeOptions(); var driver new RemoteWebDriver(new Uri(http://your-grid-hub:4444/wd/hub), options.ToCapabilities()); // 连接BrowserStack var browserstackOptions new Dictionarystring, object { [os] Windows, [os_version] 11, [browser] Chrome, [browser_version] latest, [name] My .NET Test }; options.AddAdditionalOption(bstack:options, browserstackOptions); var driver new RemoteWebDriver(new Uri(https://USERNAME:ACCESS_KEYhub.browserstack.com/wd/hub), options);这套从环境搭建、框架设计、代码编写到CI集成和问题排查的完整流程是我在多个实际项目中打磨出来的。核心思想是利用.NET Core的跨平台能力作为基础通过合理的架构设计工厂模式、依赖注入、POM来提升代码的可维护性再针对Windows和Linux环境的差异进行精细化的配置和调优。一开始可能会觉得步骤繁琐但一旦这套框架搭建完毕后续编写新的测试用例将会非常高效并且能自信地在任何支持的平台上运行。