首页
/ Playwright C 测试编写指南:自动等待、Locator 操作、Expect 断言与测试隔离

Playwright C 测试编写指南:自动等待、Locator 操作、Expect 断言与测试隔离

2026-09-06 17:28:49作者:裴麒琰

本篇指南基于 Playwright 官方文档 Writing tests (C#),系统讲解如何用 C# 编写 Playwright 端到端测试。读完后你将掌握四件事:如何用 MSTest、NUnit、xUnit 或 xUnit v3 的基类写出第一个测试;如何利用自动等待(auto-waiting)免去手动 sleep;如何用可自动重试的 Expect 断言消除竞态条件;以及如何理解测试隔离机制和测试钩子。所有代码示例均可直接复制到 .NET 项目中运行。

核心设计哲学:操作与断言

Playwright 测试的写法非常简单,它只有两个要素:

  • 执行操作(perform actions):导航、点击、填表等;
  • 断言状态(assert the state):用 Expect 校验页面是否符合预期。

与传统 E2E 框架不同,这里不需要在操作前等待任何东西

  1. 执行每个操作前,Playwright 会自动通过一组 可操作性检查(actionability checks)——元素可见、稳定、能接收事件、可用等——只有全部通过才会真正执行操作;
  2. 断言也不存在竞态条件问题,因为 Playwright 断言的设计目标就是描述"最终需要满足的期望",它会自动重试直到条件满足或超时。

正是这两个设计决策,让测试编写者可以彻底忘掉 flaky 的 Thread.Sleep 和不稳定的时序检查。

第一个 C# 测试

Playwright for .NET 不绑定特定测试框架,官方为 MSTest、NUnit、xUnit 和 xUnit v3 分别提供了基类(Microsoft.Playwright.MSTestMicrosoft.Playwright.NUnitMicrosoft.Playwright.XunitMicrosoft.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.XunitMicrosoft.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 结尾(如 GotoAsyncClickAsyncToHaveTitleAsync),返回 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.jsdocs/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:hiddenopacity: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 选项(如 ViewportSizeColorSchemeBaseURL),即传入 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# 测试代码;
  • 查看测试 TraceTrace Viewer 可查看每一步操作的截图、网络与 DOM 快照,是排查失败测试的第一手工具;
  • 在 CI 上运行CI 入门
  • 深入了解四种框架基类Test Runners (C#) 提供了 PageTest/ContextTest/BrowserTest/PlaywrightTest 的完整行为说明与 .runsettings 配置参考(BrowserNameExpectTimeoutLaunchOptions.HeadlessChannel 等)。

小结

Playwright 的 C# 测试体系可以概括为三层:基类负责隔离(每个测试一个 BrowserContext + Page,浏览器实例复用)、Locator + 自动等待负责操作(无需手动 sleep,操作前逐项通过可用性检查)、Expect 自动重试断言负责校验(默认 5 秒超时,可全局或逐条调整)。理解这三层之后,Writing tests (C#) 中的所有示例都只是同一套模式的变体;而 C# API 本身由仓库中的 utils/doclint/generateDotnetApi.jsdocs/src/api 统一生成,保证了与 Playwright 其他语言绑定在行为上的一致性。

登录后查看全文
热门项目推荐
相关项目推荐