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

基于.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)来操控具体的浏览器。不同的浏览器需要不同的驱动。

  1. Chrome/Chromium系浏览器:这是目前最主流的选择,兼容性好,社区资源丰富。你需要两个东西:

    • 浏览器本体:在Windows上直接安装;在Ubuntu上,可以使用sudo apt install google-chrome-stable安装稳定版。
    • ChromeDriver:这是关键。它的版本必须与已安装的Chrome浏览器主版本号严格匹配。例如,Chrome版本是 114.0.5735.90,那么ChromeDriver也必须是114.x.x.x版本。
  2. Firefox浏览器:另一个流行的选择,驱动名为geckodriver。它的版本兼容性要求相对宽松一些,但依然建议使用较新的、相互匹配的版本。

驱动管理的最佳实践:手动下载并配置PATH是一种方式,但在自动化脚本和CI/CD中更推荐程序化管理。我们可以使用WebDriverManager这样的第三方库(在.NET中对应的NuGet包是WebDriverManager)。它能在运行时自动检测系统已安装的浏览器版本,并下载匹配的驱动程序,极大地简化了环境配置。我们后续会详细展开如何使用。

2.3 项目创建与NuGet包引用

一切就绪后,我们开始创建测试项目。打开终端(Windows用PowerShell或CMD,Linux用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工厂

直接在每个测试方法里写死创建ChromeDriverFirefoxDriver的代码是难以维护的,更无法适应跨平台需求。我们需要一个工厂模式(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("--headless=new"); // 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("--width=1920"); options.AddArgument("--height=1080"); // 使用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.AddSingleton<IConfiguration>(config); // 注册WebDriver工厂(每次请求新实例) services.AddScoped<WebDriverFactory>(); // 注册IWebDriver,其生命周期为Scoped(每个测试用例一个) services.AddScoped<IWebDriver>(sp => { var factory = sp.GetRequiredService<WebDriverFactory>(); return factory.CreateDriver(); }); // 注册你的页面对象模型(Page Objects)或其他服务 // services.AddScoped<HomePage>(); 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[type='submit']"); 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 test

dotnet test命令会自动发现并执行项目中的所有测试。如果你想运行特定类别的测试,可以使用--filter参数:

dotnet test --filter "Category=Smoke"

跨平台文件路径处理:如果你的测试涉及上传文件,需要特别注意文件路径。在C#中,可以使用Path.CombinePath.DirectorySeparatorChar来构建跨平台兼容的路径。

string baseDirectory = AppContext.BaseDirectory; string testDataPath = Path.Combine(baseDirectory, "TestData", "sample.pdf"); // 在Windows上可能是:C:\...\TestData\sample.pdf // 在Linux上可能是:/home/.../TestData/sample.pdf

5.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/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v3 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/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v3 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 [arch=amd64] 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: 'true'

CI配置解析

  1. 两个Job并行test-on-windowstest-on-ubuntu两个任务独立运行,加快反馈速度。
  2. 环境准备
    • Windows Runner使用Chocolatey (choco)快速安装Chrome。
    • Ubuntu Runner通过添加Google官方源来安装Chrome。
  3. 环境变量:通过env设置SeleniumSettings__Headlesstrue,强制使用无头模式,这是服务器环境的标准做法。配置系统(如IConfiguration)会自动读取这些环境变量,覆盖appsettings.json中的设置。
  4. 测试过滤:使用--filter "Category!=Slow"排除标记为Slow的测试用例,保证CI流程快速完成。

5.3 测试报告与日志记录

在CI中,清晰的测试报告至关重要。除了dotnet test自带的输出,我们可以集成NUnit的XML报告生成器,并结合Allure等工具生成更美观的报告。

首先,添加报告生成器包:

dotnet add package NUnitXml.TestLogger

然后,在运行测试时指定日志格式:

dotnet test --logger:"nunit;LogFilePath=TestResult.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.使用显式等待,而非ImplicitWaitThread.Sleep
2. 增加等待超时时间。
3. 在无头模式下,尝试添加--window-size=1920,1080参数,确保布局与有头模式一致。
脚本执行超时页面JS复杂,或网络慢。同上,或系统资源(CPU/内存)不足。1. 调整PageLoadTimeoutScriptTimeout
2. 优化测试用例,减少不必要的页面跳转。
3. 升级CI Runner的配置。
字体渲染或布局差异不同系统默认字体不同。Linux服务器可能缺少中文字体。1. 对于视觉回归测试,需统一测试环境。
2. 在Linux Docker镜像中安装所需字体包:apt-get install -y fonts-wqy-zenhei

关键心得Linux无头模式是问题高发区。绝大多数在Windows上运行良好,一到Linux CI就失败的测试,问题都出在浏览器启动参数、资源限制或等待策略上。务必在Linux环境下进行充分的调试,可以使用--headless=false先以有头模式运行,观察浏览器行为,再切换到无头模式。

6.2 测试稳定性与性能优化

  1. 等待策略是稳定的基石:彻底抛弃Thread.Sleep。混合使用显式等待(针对特定条件)和隐式等待(设置一个全局的查找元素超时)。显式等待应作为首选。
  2. 使用更稳定的定位器:优先使用IdName,其次是CssSelectorXPath。避免使用依赖于页面结构顺序的索引定位(如div[3]/span[2]),这类定位在UI微调时极易失效。
  3. 测试数据隔离:每个测试用例应该使用独立的数据,避免因数据残留导致测试间相互影响。可以利用数据库事务、API清理或在测试前后执行特定的数据准备/清理脚本。
  4. 并行测试执行:NUnit和xUnit都支持并行执行。合理使用[Parallelizable]属性可以大幅缩短测试套件的总运行时间。注意,并行执行时,必须确保测试用例之间是独立的,不能共享IWebDriver实例(这就是为什么我们之前用Scoped生命周期)。
  5. 减少不必要的浏览器启动:启动浏览器是昂贵的操作。可以考虑使用[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 [arch=amd64] 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 --from=build /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 Dictionary<string, 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_KEY@hub.browserstack.com/wd/hub"), options);

这套从环境搭建、框架设计、代码编写到CI集成和问题排查的完整流程,是我在多个实际项目中打磨出来的。核心思想是利用.NET Core的跨平台能力作为基础,通过合理的架构设计(工厂模式、依赖注入、POM)来提升代码的可维护性,再针对Windows和Linux环境的差异进行精细化的配置和调优。一开始可能会觉得步骤繁琐,但一旦这套框架搭建完毕,后续编写新的测试用例将会非常高效,并且能自信地在任何支持的平台上运行。