Testes end-to-end com Playwright for .NET
Objective
Testes unitários, WebApplicationFactory e bUnit param antes de um navegador real.
Um teste end-to-end conduz o Chromium contra o app rodando, com JavaScript, CSS e rede
de verdade, e por isso pega o que as camadas de baixo não pegam: um botão que existe
mas não dá para clicar, um script que nunca carrega. O objetivo é escrever testes
Playwright for .NET com MSTest 4 contra um app ASP.NET Core rodando num socket Kestrel
real (não TestServer): locators que um usuário usaria, asserções web-first que
esperam, um trace para ler uma falha e um Page Object para que o DOM fique numa classe
só. Também cobre uma armadilha real de compatibilidade: o pacote
Microsoft.Playwright.MSTest não funciona com MSTest 4.
Use Cases
- Provar que a página, o script e a API funcionam juntos num fluxo crítico (adicionar uma tarefa, concluir, ver o total).
- Checar comportamento que só um navegador tem: foco, visibilidade, uma resposta lenta seguida de renderização, um reload que mantém os dados.
- Reproduzir uma falha de CI a partir de um trace (screenshots, snapshots do DOM, rede) em vez de um stack trace e um palpite.
- Rodar poucos fluxos de fumaça depois de um deploy enquanto os muitos testes rápidos ficam nas camadas unitária e de integração.
Deep Dive
O pacote que descobre 0 testes
O caminho documentado é Microsoft.Playwright.MSTest e uma classe base PageTest. Com
MSTest 4.5.3 no SDK .NET 11 GA, um projeto que referencia MSTest 4.5.3 e
Microsoft.Playwright.MSTest 1.64.1 compila, imprime o aviso MSTEST0082 para os membros
herdados com [TestInitialize] e [TestCleanup], e o dotnet test responde "No test is
available". A causa está nas referências do assembly: Microsoft.Playwright.MSTest.dll
referencia Microsoft.VisualStudio.TestPlatform.TestFramework 14.0.0.0, o nome de assembly
do MSTest 2.x, enquanto o MSTest 4 traz MSTest.TestFramework. O MSTest compara seus
atributos por identidade exata de tipo, então os hooks do PageTest são ignorados em
silêncio, e o adapter não consegue carregar o assembly antigo ao descobrir os testes. O
nuspec do pacote ainda depende do MSTest 2.2.7. Trocar o MSTest pela 3.11.1 faz o mesmo
teste passar, o que confirma a causa.
A saída com MSTest 4 é referenciar o pacote core Microsoft.Playwright (1.64.1) e escrever
você mesmo a classe base pequena:
plaintext<PackageReference Include="MSTest" Version="4.5.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.14" /> <PackageReference Include="Microsoft.Playwright" Version="1.64.1" />
São cerca de 30 linhas, mostradas abaixo, e tiram a dependência de um pacote que está atrás do framework.
Instale o navegador uma vez
O Playwright usa seus próprios navegadores. Compilar o projeto de teste copia o
playwright.ps1 para junto do assembly; rode-o uma vez:
plaintextdotnet build pwsh bin/Debug/net10.0/playwright.ps1 install chromium
Sem PowerShell, um console app que chama
Microsoft.Playwright.Program.Main(["install", "chromium"]) faz o mesmo (foi assim que o
lab foi verificado). Use PLAYWRIGHT_BROWSERS_PATH para escolher onde os navegadores
ficam. O teste só precisa do Chromium; no CI, instale antes do dotnet test e faça cache
da pasta.
Kestrel real, não TestServer
O WebApplicationFactory hospeda o app no TestServer por padrão, que não tem socket:
um navegador não consegue abri-lo. Suba o Kestrel no lugar, uma vez por execução:
csharp[TestClass]
public static class TestEnvironment
{
private static WebApplicationFactory<Program>? _factory;
private static IPlaywright? _playwright;
public static string BaseUrl { get; private set; } = "";
public static IBrowser Browser { get; private set; } = null!;
public static TaskStore Store => _factory!.Services.GetRequiredService<TaskStore>();
[AssemblyInitialize]
public static async Task Start(TestContext _)
{
_factory = new WebApplicationFactory<Program>();
_factory.UseKestrel(0); // uma porta livre em 127.0.0.1
_factory.StartServer();
BaseUrl = _factory.CreateClient().BaseAddress!.ToString();
_playwright = await Playwright.CreateAsync();
Browser = await _playwright.Chromium.LaunchAsync(new() { Headless = true });
}
[AssemblyCleanup]
public static async Task Stop()
{
await Browser.CloseAsync();
_playwright?.Dispose();
if (_factory is not null) await _factory.DisposeAsync();
}
}UseKestrel(0) (novo no .NET 10, veja o concept de WebApplicationFactory) escolhe uma
porta livre, e o BaseAddress do client diz qual. Um host e um processo de navegador
atendem a execução inteira; subir um navegador por teste é o erro lento. Como o host é
compartilhado, os testes zeram o estado do servidor (aqui o singleton TaskStore) antes
de cada um.
Uma classe base: um contexto por teste, um trace quando falha
O substituto do PageTest é uma classe base com hooks do MSTest. Um contexto de navegador
é barato e isolado (cookies e storage próprios), então cada teste ganha um novo. O trace
grava screenshots e snapshots do DOM e só é salvo quando o teste não passou:
csharppublic abstract class E2ETest
{
public TestContext TestContext { get; set; } = null!;
protected IBrowserContext Context { get; private set; } = null!;
protected IPage Page { get; private set; } = null!;
[TestInitialize]
public async Task OpenBrowserContext()
{
TestEnvironment.Store.Clear();
Context = await TestEnvironment.Browser.NewContextAsync(new() { BaseURL = TestEnvironment.BaseUrl });
await Context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true });
Page = await Context.NewPageAsync();
}
[TestCleanup]
public async Task CloseBrowserContext()
{
var failed = TestContext.CurrentTestOutcome != UnitTestOutcome.Passed;
await Context.Tracing.StopAsync(failed
? new() { Path = Path.Combine(TestContext.TestResultsDirectory!, $"{TestContext.TestName}.zip") }
: null);
await Context.CloseAsync();
}
}Um teste que falha deixa <NomeDoTeste>.zip na pasta de resultados (com as configurações
padrão, junto do assembly de teste). Abra com playwright.ps1 show-trace arquivo.zip: uma
linha do tempo com cada ação, a página antes e depois, o console e a rede.
Locators e asserções web-first
Um locator descreve como um usuário encontra um elemento, e é preguiçoso: nada é procurado até uma ação ou asserção usá-lo. Prefira role, label, texto e test id a CSS:
csharpPage.GetByRole(AriaRole.Button, new() { Name = "Add task" });
Page.GetByLabel("New task", new() { Exact = true });
Page.GetByTestId("open-count");Esses sobrevivem a uma mudança de estilo e funcionam como checagem de acessibilidade, já
que um elemento sem role ou label também é difícil de achar para um leitor de tela. As
ações (ClickAsync, FillAsync, CheckAsync) esperam o elemento estar anexado, visível,
estável e habilitado antes de agir. As asserções usam Expect, que tenta de novo até
passar ou estourar o tempo (5 segundos por padrão):
csharpusing static Microsoft.Playwright.Assertions;
await Expect(Tasks.Loading).ToBeVisibleAsync();
await Expect(Tasks.Loading).ToBeHiddenAsync();
await Expect(Tasks.OpenCount).ToHaveTextAsync("0 open");A página do lab mostra "Loading tasks..." por 300 ms porque a API é lenta de propósito. O
teste não precisa de sleep: cada Expect espera a página chegar. Não leia um valor uma vez
e faça Assert.AreEqual nele (await locator.TextContentAsync()): esse retrato não tem
retry e é a maior fonte de testes E2E instáveis.
O auto-wait tem limites, que aparecem como falhas reais. No lab, um teste adicionou duas
tarefas em seguida. A página limpa o input quando o primeiro POST volta, então o título da
segunda tarefa, digitado cedo demais, foi apagado, e o teste falhou com "expected count 2,
was 1". A correção é um ponto de sincronização que o usuário também teria: esperar a
primeira tarefa aparecer antes de digitar a segunda. Outra armadilha é a asserção que passa
pelo motivo errado: a lista fica escondida até carregar, então ToHaveCountAsync(0) é
verdadeira antes de qualquer coisa chegar; espere o "0 open" primeiro.
Page Object: o DOM numa classe
Quando dez testes repetem GetByRole(AriaRole.Button, new() { Name = "Add task" }),
renomear esse botão quebra dez testes. Um Page Object é dono dos locators e das ações no
nível do usuário, e os testes dizem o que uma pessoa faz:
csharppublic sealed class TasksPage(IPage page)
{
public ILocator Items => page.GetByRole(AriaRole.List, new() { Name = "Task list" }).GetByRole(AriaRole.Listitem);
public ILocator OpenCount => page.GetByTestId("open-count");
public async Task OpenAsync() => await page.GotoAsync("/");
public async Task AddAsync(string title)
{
await page.GetByLabel("New task", new() { Exact = true }).FillAsync(title);
await page.GetByRole(AriaRole.Button, new() { Name = "Add task" }).ClickAsync();
}
}csharpawait Tasks.OpenAsync();
await Tasks.AddAsync("Write the report");
await Expect(Tasks.Items).ToHaveCountAsync(1);Mantenha as asserções no teste, não no Page Object: o objeto expõe locators e ações, e o teste decide o que checar. A suíte do lab tem seis testes e roda em cerca de 6 segundos.
Trade-offs
- E2E é a camada mais lenta e menos precisa. Uma falha diz "algo neste fluxo quebrou",
não qual linha. Guarde o E2E para poucos fluxos críticos; teste regras em testes unitários,
o contrato HTTP com
WebApplicationFactorye componentes com bUnit. - bUnit vs Playwright. O bUnit renderiza um componente em milissegundos, sem navegador, e não enxerga CSS, navegação real nem interop de JavaScript. O Playwright enxerga tudo isso e custa um navegador. Use os dois no Blazor.
- Host e navegador compartilhados são rápidos e pedem disciplina. Zerar o estado do servidor por teste e usar um contexto por teste dá isolamento. Rodar os testes E2E em paralelo exige dados por teste (títulos únicos) porque o store é compartilhado.
- Seletores prendem os testes à marcação. Locators por role e label prendem ao que o usuário percebe, que muda menos, mas um botão renomeado na UI ainda os quebra. O Page Object mantém isso em um arquivo.
- Navegadores são uma dependência. O passo de instalação, o cache e a versão fixada pelo pacote (a 1.64.1 corresponde a um build específico do Chromium) precisam ser tratados em toda máquina de desenvolvimento e no CI. O pacote e o navegador andam juntos.
Microsoft.Playwright.MSTesté feito para a identidade antiga do assembly do MSTest. Com o MSTest 3.11.1, oPageTestfunciona (verificado). No MSTest 4, use o pacote core e sua própria classe base, e confira as notas de versão do pacote Playwright antes de abandonar a classe base.- O Selenium ainda existe. O auto-wait, o tracing e os contextos por teste do Playwright são os motivos de equipes migrarem do Selenium; veja o concept de Selenium para o modelo mais antigo.