Playwright C 测试编写指南:自动等待、Locator 操作、Expect 断言与测试隔离
本篇指南基于 Playwright 官方文档 Writing tests (C#),系统讲解如何用 C# 编写 Playwright 端到端测试。读完后你将掌握四件事:如何用 MSTest、NUnit、xUnit 或 xUnit v3 的基类写出第一个测试;如何利用自动等待(auto-waiting)免去手动 sleep;如何用可自动重试的 Expect 断言消除竞态条件;以及如何理解测试隔离机制和测试钩子。所有代码示例均可直接复制到 .NET 项目中运行。
核心设计哲学:操作与断言
Playwright 测试的写法非常简单,它只有两个要素:
- 执行操作(perform actions):导航、点击、填表等;
- 断言状态(assert the state):用
Expect校验页面是否符合预期。
与传统 E2E 框架不同,这里不需要在操作前等待任何东西:
- 执行每个操作前,Playwright 会自动通过一组 可操作性检查(actionability checks)——元素可见、稳定、能接收事件、可用等——只有全部通过才会真正执行操作;
- 断言也不存在竞态条件问题,因为 Playwright 断言的设计目标就是描述"最终需要满足的期望",它会自动重试直到条件满足或超时。
正是这两个设计决策,让测试编写者可以彻底忘掉 flaky 的 Thread.Sleep 和不稳定的时序检查。
第一个 C# 测试
Playwright for .NET 不绑定特定测试框架,官方为 MSTest、NUnit、xUnit 和 xUnit v3 分别提供了基类(Microsoft.Playwright.MSTest、Microsoft.Playwright.NUnit、Microsoft.Playwright.Xunit、Microsoft.Playwright.Xunit.v3 包),继承 PageTest 是最简单的方式。以下是官方文档中的完整示例,四种框架下写法几乎一致,仅命名空间和特性标注不同。
MSTest 版本:
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class ExampleTest : PageTest
{
[TestMethod]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
[TestMethod]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
NUnit 版本(NUnit 支持文件内并行,需标注 [Parallelizable(ParallelScope.Self)]):
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class ExampleTest : PageTest
{
[Test]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
[Test]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
xUnit 与 xUnit v3 版本(仅需更换命名空间:Microsoft.Playwright.Xunit 或 Microsoft.Playwright.Xunit.v3):
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
namespace PlaywrightTests;
public class UnitTest1 : PageTest
{
[Fact]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
[Fact]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
注意几个 C# API 特征:所有异步方法均以 Async 结尾(如 GotoAsync、ClickAsync、ToHaveTitleAsync),返回 Task;标题断言支持传入 Regex 实现"包含子串"匹配。
操作(Actions)
导航
大多数测试以导航到某个 URL 开始,之后才能与页面元素交互:
await Page.GotoAsync("https://playwright.dev");
Playwright 会等待页面达到 load 状态后才继续往下执行,因此导航后无需再等待 DOM 就绪。
交互:从定位器到操作
执行操作的第一步是定位元素。Playwright 使用 Locators API——Locator 代表"在任意时刻查找页面上一个或多个元素"的方式,且 Playwright 会在操作前自动等待元素 actionable,因此你不需要手动等待元素出现:
// Create a locator.
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Click it.
await getStarted.ClickAsync();
多数场景会写成一行的链式调用:
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
基础操作一览
以下是官方文档列出的最常用 Locator 操作(完整清单见 Locator API):
| 操作 | 说明 |
|---|---|
CheckAsync |
勾选复选框 |
ClickAsync |
点击元素 |
UncheckAsync |
取消勾选复选框 |
HoverAsync |
鼠标悬停在元素上 |
FillAsync |
填充表单字段、输入文本 |
FocusAsync |
聚焦元素 |
PressAsync |
按下单个按键 |
SetInputFilesAsync |
选择文件上传 |
SelectOptionAsync |
在下拉框中选择选项 |
从源码结构看,这些方法的 C# 签名并非手写,而是由 utils/doclint/generateDotnetApi.js 从 docs/src/api 目录下的多语言 API 文档统一生成:该脚本解析 markdown 中的 method: Locator.click 等条目,将 async 方法追加 Async 后缀、把返回类型映射为 Task/Task<T>,并生成带 XML 文档注释的接口(如 ILocator.ClickAsync)。这解释了为什么 C# 端与 JS/Python/Java 端的 API 在语义上严格一一对应。
自动等待的具体检查项
官方文档中"await 即可"的前提,来自 Auto-waiting 文档定义的逐项检查。例如对 Locator.click,Playwright 会确保:
- 定位器恰好解析出一个元素;
- 元素可见(有非空包围盒、无
visibility:hidden;opacity:0仍算可见,display:none不算); - 元素稳定(连续两帧动画期间包围盒不变);
- 元素能接收事件(点击点没有被其他元素遮挡);
- 元素可用(非
disabled、非[aria-disabled=true]后代等)。
不同操作的检查项不同,文档给出了完整对照表(摘录):
| 操作 | 可见 | 稳定 | 接收事件 | 可用 | 可编辑 |
|---|---|---|---|---|---|
check / click / uncheck |
Yes | Yes | Yes | Yes | - |
hover |
Yes | Yes | Yes | - | - |
fill / clear |
Yes | - | - | Yes | Yes |
selectOption |
Yes | - | - | Yes | - |
focus / press / setInputFiles |
- | - | - | - | - |
部分操作(如 click)支持 force 选项关闭非必需的可用性检查;检查在 timeout 内不通过则抛出 TimeoutError。
断言(Assertions)
Playwright 提供 Expect 函数用于断言,并自动等待直到期望条件满足:
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
最常用的一组 Locator/Page 断言如下(更多见 Assertions):
| 断言 | 说明 |
|---|---|
ToBeCheckedAsync |
复选框已勾选 |
ToBeEnabledAsync |
控件可用 |
ToBeVisibleAsync |
元素可见 |
ToContainTextAsync |
元素包含文本 |
ToHaveAttributeAsync |
元素具有某属性 |
ToHaveCountAsync |
元素列表长度为指定值 |
ToHaveTextAsync |
元素文本匹配 |
ToHaveValueAsync |
输入元素具有某值 |
ToHaveTitleAsync |
页面标题匹配 |
ToHaveURLAsync |
页面 URL 匹配 |
这些断言的关键特性是自动重试:Playwright 会反复重新获取元素并检查,直到条件满足或断言超时,因此天然消除"元素还没渲染完就断言"这类竞态。结合断言文档可以补充两个实用细节:
- 默认超时 5 秒;全局调整可在测试初始化中调用
SetDefaultExpectTimeout(10_000)(MSTest 中放在[ClassInitialize],NUnit 中放在[OneTimeSetUp]),单条断言则通过选项覆盖,例如await Expect(Page.GetByText("Name")).ToBeVisibleAsync(new() { Timeout = 10_000 });; - 自定义失败消息:
await Expect(Page.GetByText("Name"), "should be logged in").ToBeVisibleAsync();,断言失败时异常信息会包含该消息和完整的 Call log,便于定位。
测试隔离(Test Isolation)
MSTest、NUnit、xUnit 与 xUnit v3 的 Playwright 基类会将每个测试相互隔离:每个测试都能拿到一个独立的 Page 实例。隔离由 BrowserContext 保证——它相当于一个全新的浏览器配置档(brand new browser profile),因此即使多个测试复用同一个浏览器进程,每个测试仍然运行在全新的环境中(无残留 Cookie、缓存、LocalStorage)。
文档示例(MSTest):
using System.Threading.Tasks;
using Microsoft.Playwright.MSTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class ExampleTest : PageTest
{
[TestMethod]
public async Task BasicTest()
{
await Page.GotoAsync("https://playwright.dev");
}
}
NUnit、xUnit、xUnit v3 的写法与"第一个测试"一节相同,仅特性标注与命名空间不同(Page.GotoAsync 之外无需额外代码)。
与隔离相关的还有四个基类层次(来自 Test Runners 文档):
| 基类 | 说明 |
|---|---|
PageTest |
每个测试获得一个位于独立 BrowserContext 中的全新 Page,写 Playwright 测试最简单的方式 |
ContextTest |
每个测试获得一个全新 BrowserContext,可自由创建多个 Page,适合多标签页场景 |
BrowserTest |
每个测试获得一个浏览器实例,可创建任意数量的 context,需自行清理 |
PlaywrightTest |
每个测试获得 Playwright 对象,可启动/停止任意多个浏览器 |
对 PageTest/ContextTest,还可通过重写 ContextOptions() 方法按文件定制 context 选项(如 ViewportSize、ColorScheme、BaseURL),即传入 Browser.NewContext 的那组参数。
测试钩子(Test Hooks)
各框架的"每个测试前后"钩子不同,官方文档给出了对应写法。
NUnit:SetUp / TearDown
using System.Threading.Tasks;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class ExampleTest : PageTest
{
[Test]
public async Task MainNavigation()
{
// Assertions use the expect API.
await Expect(Page).ToHaveURLAsync("https://playwright.dev/");
}
[SetUp]
public async Task SetUp()
{
await Page.GotoAsync("https://playwright.dev");
}
}
MSTest:TestInitialize / TestCleanup
using System.Threading.Tasks;
using Microsoft.Playwright.MSTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class ExampleTest : PageTest
{
[TestMethod]
public async Task MainNavigation()
{
// Assertions use the expect API.
await Expect(Page).ToHaveURLAsync("https://playwright.dev/");
}
[TestInitialize]
public async Task TestInitialize()
{
await Page.GotoAsync("https://playwright.dev");
}
}
xUnit / xUnit v3:重写 InitializeAsync / DisposeAsync(必须调用 base,否则基类不会创建/释放浏览器资源):
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
namespace PlaywrightTests;
public class UnitTest1 : PageTest
{
[Fact]
public async Task MainNavigation()
{
// Assertions use the expect API.
await Expect(Page).ToHaveURLAsync("https://playwright.dev/");
}
override public async Task InitializeAsync()
{
await base.InitializeAsync();
await Page.GotoAsync("https://playwright.dev");
}
public override async Task DisposeAsync()
{
Console.WriteLine("After each test cleanup");
await base.DisposeAsync();
}
}
xUnit v3 的代码完全相同,只需把命名空间换成 Microsoft.Playwright.Xunit.v3。
运行、并行与后续学习
- 运行与调试:
dotnet test默认在 headless 模式下运行 Chromium,可指定单个测试、多个测试或以有头模式运行,详见 Running and Debugging Tests (C#); - 并行执行:各框架默认在类/文件之间并行、类内部串行(NUnit 仅支持
ParallelScope.Self,MSTest 不支持方法级并行),可通过 runsettings 或 CLI 参数调节 worker 数,浏览器与浏览器实例在测试之间复用以提升性能,详见 Test Runners; - 用 Codegen 生成测试:Codegen 入门 可以录制操作并直接产出上述风格的 C# 测试代码;
- 查看测试 Trace:Trace Viewer 可查看每一步操作的截图、网络与 DOM 快照,是排查失败测试的第一手工具;
- 在 CI 上运行:CI 入门;
- 深入了解四种框架基类:Test Runners (C#) 提供了
PageTest/ContextTest/BrowserTest/PlaywrightTest的完整行为说明与.runsettings配置参考(BrowserName、ExpectTimeout、LaunchOptions.Headless、Channel等)。
小结
Playwright 的 C# 测试体系可以概括为三层:基类负责隔离(每个测试一个 BrowserContext + Page,浏览器实例复用)、Locator + 自动等待负责操作(无需手动 sleep,操作前逐项通过可用性检查)、Expect 自动重试断言负责校验(默认 5 秒超时,可全局或逐条调整)。理解这三层之后,Writing tests (C#) 中的所有示例都只是同一套模式的变体;而 C# API 本身由仓库中的 utils/doclint/generateDotnetApi.js 从 docs/src/api 统一生成,保证了与 Playwright 其他语言绑定在行为上的一致性。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00