Skip to content
Go back

更新到 .NET 8,更新到 IHostBuilder,并在任何操作系统上无头或有头运行 NUnit 中的 Playwright 测试

摘要

我不仅仅是为我的网站进行单元测试,而是进行全面的集成…

原文 Updating to .NET 8, updating to IHostBuilder, and running Playwright Tests within NUnit headless or headed on any OS - Scott Hanselman’s BlogScott Hanselman 发表。


所有单元测试都通过

我不仅仅为我的网站做单元测试,自2007年以来,我一直在进行全面的集成测试和浏览器自动化测试,最初使用Selenium。然而,最近我一直在使用更快且通常更兼容的 Playwright。它具有一个API,并且可以在Windows、Linux、Mac上本地测试,在容器中(无头)、在我的CI/CD管道上、在Azure DevOps上,或在GitHub Actions中测试。

对我来说,这是确保网站从头到尾完全运行的最后一个真理时刻。

我可以用类似TypeScript的语言编写那些Playwright测试,并且我可以用node启动它们,但我喜欢运行端到端单元测试,并使用那个测试运行器和测试框架作为我.NET应用程序的跳板。我习惯于在Visual Studio或VS Code中右键点击并选择“运行单元测试”,或者更好地,右键点击并选择“调试单元测试”。这让我得到了使用完整单元测试框架的所有断言的好处,以及使用Playwright这类工具自动化我的浏览器的所有好处。

2018年我开始使用WebApplicationFactory和一些技巧,基本上是在单元测试中启动ASP.NET(当时的)Core 2.1,然后启动Selenium。这有点笨拙,需要手动启动一个单独的进程并管理其生命周期。然而,我持续使用这个技巧好几年,基本上是试图让Kestrel Web服务器在我的单元测试中启动。

最近,我将我的主站点和播客站点升级到了.NET 8。请记住,我一直在将我的网站从早期版本的.NET向最新版本迁移。博客现在很高兴地在Linux的容器中运行在.NET 8上,但它的原始代码始于2002年的.NET 1.1。

现在我升级到.NET 8,当我的单元测试停止工作时,我震惊地发现世界其它地方五个版本前就从IWebHostBuilder转移到IHostBuilder了。哎呀。不管你怎么说,向后兼容性令人印象深刻。

因此,我的Program.cs代码从这样:

public static void Main(string[] args)
{
    CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>();

变成了这样:

public static void Main(string[] args)
{
  CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
  Host.CreateDefaultBuilder(args).
      ConfigureWebHostDefaults(WebHostBuilder => WebHostBuilder.UseStartup<Startup>());

从外表上看不是一个重大变化,但在内部整理了一下,为我的web应用设置了一个更灵活的通用主机

我的单元测试停止工作,因为我的Kestral Web服务器黑客袭击不再启动我的服务器。

下面是一个在.NET NUnit测试中的Playwright角度目标的示例。

[Test]
public async Task DoesSearchWork()
{
    await Page.GotoAsync(Url);

    await Page.Locator("#topbar").GetByRole(AriaRole.Link, new() { Name = "episodes" }).ClickAsync();

    await Page.GetByPlaceholder("search and filter").ClickAsync();

    await Page.GetByPlaceholder("search and filter").TypeAsync("wife");

    const string visibleCards = ".showCard:visible";

    var waiting = await Page.WaitForSelectorAsync(visibleCards, new PageWaitForSelectorOptions() { Timeout = 500 });

    await Expect(Page.Locator(visibleCards).First).ToBeVisibleAsync();

    await Expect(Page.Locator(visibleCards)).ToHaveCountAsync(5);
}

我喜欢这个。干净利落。当然,在第一行中我们假设有一个URL,将是某个localhost,然后我们假设我们的web应用程序已经自行启动。

下面是启动我的新的“web应用程序测试构建器工厂”的设置代码,是的,这个名字很蠢但它很形象。注意OneTimeSetUp和OneTimeTearDown。这启动了我的web应用程序在我的TestHost的上下文中。注意:0使得应用找到一个端口,然后我不得不挖掘并将其放入私有Url中,供我在我的单元测试中使用。注意实际上是我的Startup类,在Startup.cs中,它托管了我的应用程序的管道和Configure和ConfigureServices在这里设置,所以路由都工作了。

private string Url;
private WebApplication? _app = null;

[OneTimeSetUp]
public void Setup()
{
    var builder = WebApplicationTestBuilderFactory.CreateBuilder<Startup>();

    var startup = new Startup(builder.Environment);
    builder.WebHost.ConfigureKestrel(o => o.Listen(IPAddress.Loopback, 0));
    startup.ConfigureServices(builder.Services);
    _app = builder.Build();

    // 监听任何本地端口(因此是0)
    startup.Configure(_app, _app.Configuration);
    _app.Start();

    // 你在开玩笑吧
    Url = _app.Services.GetRequiredService<IServer>().Features.GetRequiredFeature<IServerAddressesFeature>().Addresses.Last();
}

[OneTimeTearDown]
public async Task TearDown()
{
    await _app.DisposeAsync();
}

那么WebApplicationTestBuilderFactory中隐藏了什么恐怖?第一部分很糟,我们应该在.NET 9中修复它。其余的实际上很好,感谢David Fowler的帮助和指导!这是魔法和不足之处在一个小助手类中。

public class WebApplicationTestBuilderFactory
{
    public static WebApplicationBuilder CreateBuilder<T>() where T : class
    {
        // 这段不神圣的代码需要一个对MvcTesting包的未使用的引用,该包通过MSBuild创建
        // 读取这里的清单文件。
        var testLocation = Path.Combine(AppContext.BaseDirectory, "MvcTestingAppManifest.json");
        var json = JsonObject.Parse(File.ReadAllText(testLocation));
        var asmFullName = typeof(T).Assembly.FullName ?? throw new InvalidOperationException("Assembly Full Name is null");
        var contentRootPath = json?[asmFullName]?.GetValue<string>();

        // 在TestHost.exe中启动一个真实的web应用程序
        var builder = WebApplication.CreateBuilder(
            new WebApplicationOptions()
            {
                ContentRootPath = contentRootPath,
                ApplicationName = asmFullName
            });
        return builder;
    }
}

前4行很糟糕。因为测试运行在不同目录的上下文中,而我的网站需要在其自己的内容根路径上下文中运行,我必须强制内容根路径正确,而唯一的方法就是从MSBuild生成的文件中获取应用程序的基目录,该文件来自(老化的)MvcTesting包。该包未被使用,但通过引用它,它就进入了构建,并生成了那个文件,然后我使用它来抽取目录。

如果我们能够摆脱那个“黑客”并从其他地方获取目录,那么这个辅助函数变成了一行代码,.NET 9的可测试性将大大提高!

现在我可以跨所有操作系统运行我的单元测试和Playwright浏览器集成测试,无头或有头,在docker中或在金属上。网站已更新到.NET 8,我的代码一切正常。嗯,至少它运行了。;)


标签


Previous Post
使用YARP负载均衡实现ASP.NET Core APIs的水平扩展
Next Post
Elmo - Your AI web copilot,一个用于创建摘要、洞见和拓展知识的Chrome扩展