.NET 版入门与差异
本教程共 59 篇 · 第 58 篇 · 更新于 2026-08-04 · 约 11 分钟阅读
本节目标:在 .NET 项目里装好 Playwright,用 MSTest / NUnit / xUnit 任一框架跑通第一个测试。
.NET 版跟 Python 版思路一样:Playwright 只提供库,测试运行交给你熟悉的框架。
区别在于官方多给了一层基类。继承它,Page 和 Expect 就白送给你了。
安装
先建一个测试项目。三种框架挑一个,命令不一样。
# MSTest
dotnet new mstest -n PlaywrightTests
# NUnit
dotnet new nunit -n PlaywrightTests
# xUnit
dotnet new xunit -n PlaywrightTests
进目录,装对应的 NuGet 包。包名跟框架一一对应,别装错。
cd PlaywrightTests
dotnet add package Microsoft.Playwright.MSTest # MSTest 用这个
dotnet add package Microsoft.Playwright.NUnit # NUnit 用这个
dotnet add package Microsoft.Playwright.Xunit # xUnit 用这个
这三个包都依赖核心包 Microsoft.Playwright,会自动带下来。只做纯脚本自动化、不写测试,才单独装核心包。
编译一次,然后下载浏览器:
dotnet build
pwsh bin/Debug/net8.0/playwright.ps1 install
Note第二条命令要用 PowerShell(
pwsh)。它是构建时生成的脚本,路径里的net8.0要换成你项目实际的目标框架。没装 PowerShell 的话,先dotnet tool install --global PowerShell。
Warning必须先
dotnet build再执行playwright.ps1。没构建过就没有这个脚本文件,新手常在这里报「找不到路径」。
MSTest 集成
继承 PageTest 基类,测试方法里直接用 Page。
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace PlaywrightTests;
[TestClass]
public class ExampleTest : PageTest
{
[TestMethod]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
[TestMethod]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
Page 和 Expect 都来自基类,不用自己创建、也不用手动关闭。每个测试方法自动拿到独立的浏览器上下文。
跑测试就是标准命令:
dotnet test
NUnit 集成
代码几乎一样,只有特性(Attribute)名字换了。
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class ExampleTest : PageTest
{
[Test]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
}
[Parallelizable(ParallelScope.Self)] 是 NUnit 的并行开关。加上它,同一个类里的测试才能并发跑。
xUnit 集成
xUnit 用 [Fact] 标记测试,类上不需要额外特性。
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
namespace PlaywrightTests;
public class ExampleTest : PageTest
{
[Fact]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
}
xUnit 默认按测试类并行,同一个类内部串行。这跟 NUnit 的默认行为正好相反。
Tip1.62.x 同时支持 xUnit v2 和 xUnit v3。用 v3 的话,装
Microsoft.Playwright.Xunit.v3这个包(注意末尾是小写v3)。具体包名以 NuGet 上的实际发布为准。
四个基类怎么选
官方给了一组基类,粒度从粗到细。继承越具体的,拿到的现成东西越多。
| 基类 | 提供什么 | 什么时候用 |
|---|---|---|
PageTest | Page + Context + Browser + Expect | 绝大多数 UI 测试,默认选它 |
ContextTest | Context + Browser | 一个测试里要开多个页面 |
BrowserTest | Browser | 要自己控制上下文配置 |
PlaywrightTest | Playwright 实例 | 只做 API 测试,或完全自定义 |
层级关系是包含式的:PageTest 里也能拿到 Context 和 Browser。
不确定就用 PageTest。它覆盖九成场景。
命名与传参差异
C# 版的写法差异集中在三点,习惯了就很规律。
方法名帕斯卡命名,全部带 Async 后缀:
await Page.GotoAsync("https://example.com");
await Page.GetByLabel("用户名").FillAsync("admin");
await Page.GetByRole(AriaRole.Button, new() { Name = "登录" }).ClickAsync();
选项用对象初始化器,new() 可以省略类型名:
// 完整写法
await Page.GotoAsync("https://example.com", new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = 15000,
});
// 简写:编译器能推断类型
await Page.GotoAsync("https://example.com", new() { WaitUntil = WaitUntilState.NetworkIdle });
枚举替代字符串。TS 里传 'button',C# 里传 AriaRole.Button。编译期就能查错,敲错了根本编译不过。
断言写法
Expect 来自基类,用法跟 TS 版对得上,只是要 await。
await Expect(Page.GetByTestId("status")).ToHaveTextAsync("成功");
await Expect(Page.GetByRole(AriaRole.Button)).ToBeEnabledAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*dashboard"));
不继承基类时,从静态类里取:
using static Microsoft.Playwright.Assertions;
await Expect(locator).ToBeVisibleAsync();
跟 Python 版一样,这些断言会自动重试,别用 Assert.AreEqual 去判断页面状态。
定制上下文配置
想改视口、语言、设备模拟,重写基类的 ContextOptions() 方法。
public class MyTest : PageTest
{
public override BrowserNewContextOptions ContextOptions()
{
return new BrowserNewContextOptions
{
Locale = "zh-CN",
ViewportSize = new() { Width = 1440, Height = 900 },
ColorScheme = ColorScheme.Dark,
};
}
}
这个方法对当前类的所有测试生效。想全局生效,就抽一个自己的基类,让测试类都继承它。
用配置文件控制运行
不改代码就想换浏览器、开有头模式,两条路。
环境变量,临时试一下最方便:
HEADED=1 dotnet test
BROWSER=webkit dotnet test
runsettings 文件,团队协作用这个更规范:
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<Playwright>
<BrowserName>chromium</BrowserName>
<LaunchOptions>
<Headless>false</Headless>
<SlowMo>250</SlowMo>
</LaunchOptions>
<ExpectTimeout>10000</ExpectTimeout>
</Playwright>
</RunSettings>
dotnet test --settings:.runsettings
也可以命令行直接传参覆盖:
dotnet test -- Playwright.BrowserName=firefox Playwright.LaunchOptions.Headless=false
并行执行
.NET 版没有内置并行,靠各测试框架自己的机制。
# NUnit:指定并发线程数
dotnet test -- NUnit.NumberOfTestWorkers=5
# MSTest:开启方法级并行
dotnet test -- MSTest.Parallelize.Workers=5 MSTest.Parallelize.Scope=method
xUnit 默认就按类并行,通常不用额外配置。
并发度别开太大。每个 worker 都要占一个浏览器上下文,机器扛不住反而更慢。
脱离测试框架直接用
只写自动化脚本、不做测试,就用核心包。这时候要自己管生命周期。
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new()
{
Headless = false,
});
var page = await browser.NewPageAsync();
await page.GotoAsync("https://playwright.dev");
await page.ScreenshotAsync(new() { Path = "example.png" });
using 和 await using 负责释放资源,比手动写 CloseAsync() 稳妥。
Browser 要用 await using,因为它的释放动作本身是异步的。Playwright 用普通 using 就够。
新手常踩的三个坑
第一个:忘了 await。 C# 里漏掉 await,方法返回一个 Task,动作根本没执行。编译器只给警告不报错,测试却诡异地全绿。
Page.GotoAsync("https://example.com"); // ❌ 没 await,等于没跑
await Page.GotoAsync("https://example.com"); // ✅
第二个:测试方法没写成 async Task。 写成 void 的话,测试框架不会等它跑完,断言失败也捕获不到。
[TestMethod]
public async Task MyTest() { } // ✅ 一律用 async Task
[TestMethod]
public async void MyTest() { } // ❌ async void 不可控
第三个:升级包后忘了重装浏览器。 NuGet 包版本变了,对应的浏览器二进制也换了。改完版本号,重新跑一遍 dotnet build 加 playwright.ps1 install。
跟 TS 版的对照速查
手边有 TS 文档想翻译成 C#,照这张表改就行。
| TypeScript | C# |
|---|---|
page.goto(url) | await Page.GotoAsync(url) |
page.getByText('登录') | Page.GetByText("登录") |
locator.click() | await locator.ClickAsync() |
expect(l).toHaveText('x') | await Expect(l).ToHaveTextAsync("x") |
{ timeout: 5000 } | new() { Timeout = 5000 } |
test.beforeEach | [TestInitialize] / [SetUp] |
规律就三条:方法加 Async 和 await、选项换成对象初始化器、钩子交给测试框架的特性。
小结
装包看框架:MSTest / NUnit / Xunit 三个包对应三种测试框架,别装混。
先 dotnet build 再跑 playwright.ps1 install,顺序反了会找不到脚本。
继承 PageTest 就能直接用 Page 和 Expect,四个基类按需要挑。
方法名全带 Async、选项用对象初始化器、角色用枚举,这是 C# 版三个固定写法。
并行和配置都交给测试框架与 runsettings,Playwright 本身不管这些。