ABP VNext + Playwright E2E:前后端一体化自动化测试 🚀
📚 目录
一、引言
✨ TL;DR
- 🔥 使用 Playwright for .NET + WebApplicationFactory 自动启动后端,实现前后端一体化 E2E 测试
- 🔒 通过
TransactionScope
(指定隔离级别)进行跨DbContext
事务隔离,测试结束自动回滚 - ✅ 覆盖登录、CRUD、API 拦截与性能断言,支持多浏览器并行 & 本地/CI 集成
- 🎥 自动录制失败 视频 与 Trace,并生成 HTML 报告,便于调试与持续集成
📚 背景与动机
在微服务与前后端分离架构下,单元测试难以捕捉 UI 与后端交互、网络请求和路由跳转等真实场景问题。借助 Playwright for .NET 的多内核自动化 + ABP VNext 的模块化后端,我们可以在“关闭真实外部依赖”的隔离环境中,完整验证业务链路,提高测试可信度与效率。
二、环境与依赖
.NET SDK:≥ 8.0(ABP VNext)
Node.js:≥ 16(Playwright 浏览器内核)
NuGet 包:
Microsoft.Playwright
Microsoft.Playwright.Xunit
(提供PageTest
、PlaywrightFact
等基类)
测试框架:xUnit
浏览器内核:Chromium、Firefox、WebKit
三、项目结构示例
MyApp/
├─ src/
│ └─ MyApp.HttpApi.Host/ # ABP 后端
├─ tests/
│ └─ E2ETests/
│ ├─ E2ETests.csproj # 测试工程
│ ├─ TestWebApplicationFactory.cs # 自定义宿主
│ ├─ TestFixture.cs # 基类:事务隔离、初始化/清理
│ ├─ LoginTests.cs # 登录测试
│ ├─ ProductCrudTests.cs # CRUD 测试
│ └─ playwright.config.js # Playwright 安装脚本
四、安装与初始化 Playwright
# 进入测试目录,安装浏览器内核(仅一次)
cd tests/E2ETests
npx playwright install
# 添加 NuGet 依赖
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.Xunit
// tests/E2ETests/playwright.config.js
module.exports = {}; // 如需自定义可在此添加
五、测试基类与数据隔离
1. 自定义 TestWebApplicationFactory 🏭
// TestWebApplicationFactory.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public class TestWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// 移除原有 DbContext 注册
var descriptor = services.Single(d => d.ServiceType == typeof(DbContextOptions<MyAppDbContext>));
services.Remove(descriptor);
// 注册测试库连接(可用环境变量动态配置)
services.AddDbContext<MyAppDbContext>(options =>
options.UseSqlServer("Server=localhost;Database=MyApp_Test;User Id=sa;Password=Your_pwd123;"));
});
}
}
2. 事务隔离 🌐
// TestFixture.cs
using System.Transactions;
using Microsoft.Playwright.Xunit;
public abstract class E2ETestBase : PageTest
{
protected TestWebApplicationFactory Factory { get; private set; }
protected HttpClient ApiClient { get; private set; }
private TransactionScope _txScope;
public override async Task OnInitializeAsync()
{
await base.OnInitializeAsync();
// 启动后端宿主
Factory = new TestWebApplicationFactory();
ApiClient = Factory.CreateClient();
// 开启事务(指定隔离级别为 ReadCommitted)
var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };
_txScope = new TransactionScope(
TransactionScopeOption.Required, options, TransactionScopeAsyncFlowOption.Enabled);
// 种子数据示例
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyAppDbContext>();
db.Products.Add(new Product { Name = "SeedProduct", Price = 1.23M });
await db.SaveChangesAsync();
}
public override async Task OnCleanupAsync()
{
await base.OnCleanupAsync(); // 先让 Playwright 清理
_txScope.Dispose(); // 回滚事务
Factory.Dispose(); // 释放宿主
}
}
💡Tips:并行执行时,可结合 Testcontainers 为每个 worker 动态创建隔离数据库,或在连接字符串中拼接线程 ID。
六、测试流程图 📝
七、登录流程测试 🔑
// LoginTests.cs
using Microsoft.Playwright.Xunit;
public class LoginTests : E2ETestBase
{
[PlaywrightFact(Timeout = 60000, Video = VideoMode.RetainOnFailure, Trace = TraceMode.On)]
public async Task Admin_Can_Login_And_See_Homepage()
{
var baseUrl = Factory.Server.BaseAddress!;
await Page.GotoAsync($"{baseUrl}/auth/login");
await Page.FillAsync("input[name=\"username\"]", "admin");
await Page.FillAsync("input[name=\"password\"]", "123qwe");
await Page.ClickAsync("button[type=\"submit\"]");
await Page.WaitForURLAsync($"{baseUrl}/");
Assert.True(await Page.Locator("text=欢迎, admin").IsVisibleAsync());
}
}
八、CRUD 操作与 API 拦截 ✂️
// ProductCrudTests.cs
using System.Diagnostics;
using Microsoft.Playwright.Xunit;
public class ProductCrudTests : E2ETestBase
{
[PlaywrightFact(Timeout = 60000, Video = VideoMode.RetainOnFailure, Trace = TraceMode.On)]
public async Task Perform_Product_CRUD_And_Assert_API_Performance()
{
var baseUrl = Factory.Server.BaseAddress!;
// 拦截 POST 并断言
await Page.RouteAsync("**/api/app/products", async route =>
{
Assert.Equal("POST", route.Request.Method);
await route.ContinueAsync();
});
// 性能断言
var sw = Stopwatch.StartNew();
await Page.GotoAsync($"{baseUrl}/products");
await Page.WaitForResponseAsync("**/api/app/products");
sw.Stop();
Assert.True(sw.ElapsedMilliseconds < 200, $"API 响应超时:{sw.ElapsedMilliseconds} ms");
// 创建
await Page.FillAsync("input[name=\"name\"]", "TestProduct");
await Page.FillAsync("input[name=\"price\"]", "9.99");
await Page.ClickAsync("button[type=\"submit\"]");
await Page.WaitForSelectorAsync("text=TestProduct");
// 更新
await Page.ClickAsync("button.edit-btn");
await Page.FillAsync("input[name=\"price\"]", "19.99");
await Page.ClickAsync("button.save-btn");
await Page.WaitForSelectorAsync("text=19.99");
// 删除
await Page.ClickAsync("button.delete-btn");
await Page.ClickAsync("button.confirm-delete");
await Page.WaitForSelectorAsync("text=No products found");
}
}
九、并行执行与多环境隔离 ⚙️
// AssemblyInfo.cs
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = false, MaxParallelThreads = 4)]
💡Tips:可通过环境变量或 Testcontainers 为每个 worker 动态生成不同的数据库实例,彻底隔离竞态。
十、CI 流水线流程图 🚦
十一、GitHub Actions & 报告集成 🎯
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET & Node.js
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'
- uses: actions/setup-node@v3
with:
node-version: '16'
- name: Install Playwright Browsers
working-directory: tests/E2ETests
run: npx playwright install --with-deps
- name: Build Backend & Tests
run: dotnet build --configuration Release
- name: Run E2E Tests
working-directory: tests/E2ETests
run: |
dotnet test --logger "trx;LogFileName=TestResults.trx" --results-directory ./results
- name: Generate Playwright HTML Report
run: |
npm install -g report-generator
reportgenerator -reports:tests/E2ETests/results/TestResults.trx -targetdir:tests/E2ETests/results/html
- name: Upload Test Artifacts
uses: actions/upload-artifact@v3
with:
name: e2e-test-report
path: |
tests/E2ETests/results/html
tests/E2ETests/results/TestResults.trx
十二、报告与调试 🐞
- 视频 & Trace:失败用例自动保留在
TestResults
,可通过npx playwright show-trace trace.zip
可视化 - 日志调试:本地运行时设置
DEBUG=pw:api
,打印 Playwright 与浏览器交互细节
十三、跨框架复用 🔄
public async Task LoginAsAsync(string user, string pwd)
{
await Page.GotoAsync($"{BaseUrl}/auth/login");
await Page.FillAsync("input[name=\"username\"]", user);
await Page.FillAsync("input[name=\"password\"]", pwd);
await Page.ClickAsync("button[type=\"submit\"]");
await Page.WaitForURLAsync($"{BaseUrl}/`);
}
结合配置文件(appsettings.Test.json
)控制不同框架的路由前缀,实现一套脚本多端复用。