Migrate all test projects to xUnit v3 and Microsoft Test Platform

Major Changes:
- Upgraded from xunit.core v2 to xunit.v3 (3.1.0)
- Replaced Microsoft.NET.Test.Sdk with Microsoft.Testing.Platform.MSBuild
  (1.8.4)
- Updated test projects to use <OutputType>Exe</OutputType> (required for
  xUnit v3)

Package Updates:
- Replaced Verify.Xunit with Verify.XunitV3 (31.0.0)
- Replaced Meziantou.Extensions.Logging.Xunit with
  MartinCostello.Logging.XUnit.v3 (0.6.0)
- Replaced Serilog.Sinks.XUnit with Serilog.Sinks.XUnit3 (1.1.0)
- Removed Xunit.SkippableFact (xUnit v3 has built-in skipping)
- Removed Microsoft.SourceLink.GitHub from test projects
- Updated Serilog to 4.3.0 (required by Serilog.Sinks.XUnit3)

Code Changes:
- Updated IAsyncLifetime implementations (Task → ValueTask,
  Task.CompletedTask → default)
- Removed 'using Xunit.Abstractions;' from 37 files
- Replaced Skip.If() with Assert.Skip() for conditional test skipping
- Replaced [SkippableFact] with [Fact] and [SkippableTheory] with [Theory]
- Updated BeforeAfterTestAttribute to IBeforeAfterTestAttribute in xUnit v3
- Fixed TheoryData syntax for xUnit v3 (collection expressions)

Playwright Integration:
- Removed Retries folder (unused xUnit v2 extensibility code)
- Replaced Microsoft.Playwright.Xunit with Microsoft.Playwright
- Removed PageTest base class, implemented Playwright directly
- Added IAsyncLifetime implementation with manual browser initialization
This commit is contained in:
Damian Hickey 2025-10-12 20:22:42 +02:00 committed by Damian Hickey
parent 0f8bbf0611
commit 0afe76f186
59 changed files with 136 additions and 368 deletions

View file

@ -39,7 +39,6 @@
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="9.5.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.0" />
<PackageVersion Include="BullsEye" Version="5.0.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<!-- Added aspire transitive package to resolve package vulnerability -->
<PackageVersion Include="KubernetesClient" Version="17.0.14" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="3.2.0" Condition="'$(IsBffProject)' == 'true'" />
@ -54,7 +53,7 @@
<PackageVersion Include="Duende.IdentityServer" Version="7.1.0" />
<PackageVersion Include="Duende.Private.Licensing" Version="1.0.0" />
<PackageVersion Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
<PackageVersion Include="Meziantou.Extensions.Logging.Xunit" Version="1.0.8" />
<PackageVersion Include="MartinCostello.Logging.XUnit.v3" Version="0.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Certificate" Version="$(AuthenticationCertificateVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(FrameworkVersion)" />
@ -98,10 +97,10 @@
<PackageVersion Include="Microsoft.IdentityModel.Logging" Version="$(IdentityModelVersion)" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(IdentityModelVersion)" />
<PackageVersion Include="Microsoft.Net.Http.Headers" Version="9.0.6" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.NETCore.Jit" Version="2.0.8" />
<PackageVersion Include="Microsoft.Playwright.Xunit" Version="1.50.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.55.0" />
<PackageVersion Include="Microsoft.Playwright.Xunit.v3" Version="1.55.0" />
<PackageVersion Include="Microsoft.Testing.Platform.MSBuild" Version="1.8.4" />
<PackageVersion Include="MinVer" Version="6.0.0" />
<PackageVersion Include="NBomber" Version="6.0.2" />
<PackageVersion Include="NBomber.Http" Version="6.0.2" />
@ -117,13 +116,13 @@
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="PublicApiGenerator" Version="11.1.0" />
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.2.0" />
<PackageVersion Include="Serilog.Sinks.TextWriter" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.XUnit" Version="3.0.19" />
<PackageVersion Include="Serilog.Sinks.XUnit3" Version="1.1.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="SimpleExec" Version="12.0.0" />
@ -131,10 +130,11 @@
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="xunit.core" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="Verify.Xunit" Version="28.9.0" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.v3" Version="3.1.0" />
<PackageVersion Include="xunit.v3.core" Version="3.1.0" />
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.1.0" />
<PackageVersion Include="Verify.XunitV3" Version="31.0.0" />
<PackageVersion Include="Vogen" Version="7.0.3" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.1.0" />
</ItemGroup>

View file

@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="9.0.4" />
<PackageReference Include="Meziantou.Extensions.Logging.Xunit" />
<PackageReference Include="MartinCostello.Logging.XUnit.v3" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="RichardSzalay.MockHttp" />

View file

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using Xunit.Abstractions;
namespace Duende.AspNetCore.Authentication.JwtBearer;

View file

@ -4,7 +4,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.AspNetCore.TestFramework;

View file

@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using RichardSzalay.MockHttp;
using Xunit.Abstractions;
namespace Duende.AspNetCore.TestFramework;

View file

@ -4,13 +4,12 @@
using System.Net;
using System.Reflection;
using System.Security.Claims;
using Meziantou.Extensions.Logging.Xunit;
using MartinCostello.Logging.XUnit;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Duende.AspNetCore.TestFramework;
@ -104,7 +103,7 @@ public class GenericHost
private void ConfigureServices(IServiceCollection services)
{
// This adds log messages to the output of our tests when they fail.
// See https://www.meziantou.net/how-to-view-logs-from-ilogger-in-xunitdotnet.htm
// See https://github.com/martincostello/xunit-logging
services.AddLogging(options =>
{
// If you need different log output to understand a test failure, configure it here
@ -113,10 +112,7 @@ public class GenericHost
options.AddFilter("Duende.IdentityServer.License", LogLevel.Error);
options.AddFilter("Duende.IdentityServer.Startup", LogLevel.Error);
options.AddProvider(new XUnitLoggerProvider(_testOutputHelper, new XUnitLoggerOptions
{
IncludeCategory = true,
}));
options.AddXUnit(_testOutputHelper);
});
OnConfigureServices(services);

View file

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Xunit.Abstractions;
namespace Duende.AspNetCore.TestFramework;

View file

@ -13,7 +13,7 @@
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="PublicApiGenerator" />
<PackageReference Include="Verify.Xunit" />
<PackageReference Include="Verify.XunitV3" />
</ItemGroup>

View file

@ -3,28 +3,16 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>
<PackageReference Include="NSubstitute" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
<PackageReference Include="PublicApiGenerator" />
<PackageReference Include="Verify.Xunit" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<PackageReference Include="Verify.XunitV3" />
</ItemGroup>
<ItemGroup>
@ -32,4 +20,8 @@
<ProjectReference Include="..\Bff.Tests\Bff.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View file

@ -6,7 +6,6 @@ using Duende.Bff;
using Duende.Bff.Tests.TestHosts;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Xunit.Abstractions;
namespace Bff.Blazor.UnitTests;
@ -70,7 +69,7 @@ public class BffBlazorTests : OutputWritingTestBase
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
public override async Task InitializeAsync()
public override async ValueTask InitializeAsync()
{
await IdentityServerHost.InitializeAsync();
await ApiHost.InitializeAsync();
@ -78,7 +77,7 @@ public class BffBlazorTests : OutputWritingTestBase
await base.InitializeAsync();
}
public override async Task DisposeAsync()
public override async ValueTask DisposeAsync()
{
await ApiHost.DisposeAsync();
await BffHost.DisposeAsync();

View file

@ -7,8 +7,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="PublicApiGenerator" />
<PackageReference Include="Verify.Xunit" />
<PackageReference Include="Verify.XunitV3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Bff.EntityFramework\Bff.EntityFramework.csproj" />
</ItemGroup>

View file

@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="PublicApiGenerator" />
<PackageReference Include="Verify.Xunit" />
<PackageReference Include="Verify.XunitV3" />
</ItemGroup>
<ItemGroup>

View file

@ -7,7 +7,6 @@ using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints;

View file

@ -6,7 +6,6 @@ using System.Net.Http.Json;
using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints;

View file

@ -4,7 +4,6 @@
using System.Net;
using Duende.Bff.Tests.TestHosts;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints.Management;

View file

@ -4,7 +4,6 @@
using System.Net;
using Duende.Bff.Tests.TestHosts;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints.Management;

View file

@ -4,7 +4,6 @@
using System.Net;
using Duende.Bff.Tests.TestHosts;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints.Management;

View file

@ -5,7 +5,6 @@ using System.Net;
using Duende.Bff.Tests.TestHosts;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints.Management;

View file

@ -4,7 +4,6 @@
using System.Net;
using System.Security.Claims;
using Duende.Bff.Tests.TestHosts;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints.Management;

View file

@ -6,7 +6,6 @@ using System.Net.Http.Json;
using System.Text.Json;
using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints;

View file

@ -4,7 +4,6 @@
using System.Net;
using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Endpoints;

View file

@ -4,7 +4,6 @@
using System.Net;
using Duende.Bff.Tests.TestFramework;
using Microsoft.AspNetCore.Builder;
using Xunit.Abstractions;
namespace Duende.Bff.Tests;

View file

@ -4,7 +4,6 @@
using System.Text.Json;
using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Headers;

View file

@ -4,7 +4,6 @@
using System.Text.Json;
using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Headers;

View file

@ -4,7 +4,6 @@
using System.Text.Json;
using Duende.Bff.Tests.TestFramework;
using Duende.Bff.Tests.TestHosts;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.Headers;

View file

@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit.Abstractions;
namespace Duende.Bff.Tests;

View file

@ -5,7 +5,6 @@ using Duende.Bff.Tests.TestHosts;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Time.Testing;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.SessionManagement;

View file

@ -4,7 +4,6 @@
using Duende.Bff.Tests.TestHosts;
using Duende.IdentityServer.Stores;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.SessionManagement;

View file

@ -3,7 +3,6 @@
using Duende.Bff.Tests.TestHosts;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.SessionManagement;

View file

@ -46,7 +46,7 @@ public class GenericHost(WriteTestOutput writeOutput, string baseAddress = "http
return _baseAddress + path;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
var hostBuilder = new HostBuilder()
.ConfigureWebHost(builder =>

View file

@ -6,7 +6,6 @@ using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.TestHosts;
@ -52,7 +51,7 @@ public class BffIntegrationTestBase : OutputWritingTestBase
public async Task Login(string sub) => await IdentityServerHost.IssueSessionCookieAsync(new Claim("sub", sub));
public override async Task InitializeAsync()
public override async ValueTask InitializeAsync()
{
await IdentityServerHost.InitializeAsync();
await ApiHost.InitializeAsync();
@ -61,7 +60,7 @@ public class BffIntegrationTestBase : OutputWritingTestBase
await base.InitializeAsync();
}
public override async Task DisposeAsync()
public override async ValueTask DisposeAsync()
{
await ApiHost.DisposeAsync();
await BffHost.DisposeAsync();

View file

@ -2,7 +2,6 @@
// See LICENSE in the project root for license information.
using System.Text;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.TestHosts;
@ -18,9 +17,9 @@ public class OutputWritingTestBase(ITestOutputHelper testOutputHelper) : IAsyncL
}
}
public virtual Task InitializeAsync() => Task.CompletedTask;
public virtual ValueTask InitializeAsync() => default;
public virtual Task DisposeAsync()
public virtual ValueTask DisposeAsync()
{
lock (_output)
{
@ -28,6 +27,6 @@ public class OutputWritingTestBase(ITestOutputHelper testOutputHelper) : IAsyncL
}
return Task.CompletedTask;
return default;
}
}

View file

@ -6,7 +6,6 @@ using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Duende.Bff.Tests.TestHosts;
@ -51,7 +50,7 @@ public class YarpBffIntegrationTestBase : OutputWritingTestBase
public async Task Login(string sub) => await _identityServerHost.IssueSessionCookieAsync(new Claim("sub", sub));
public override async Task InitializeAsync()
public override async ValueTask InitializeAsync()
{
await _identityServerHost.InitializeAsync();
await ApiHost.InitializeAsync();
@ -60,7 +59,7 @@ public class YarpBffIntegrationTestBase : OutputWritingTestBase
await base.InitializeAsync();
}
public override async Task DisposeAsync()
public override async ValueTask DisposeAsync()
{
await _identityServerHost.DisposeAsync();
await ApiHost.DisposeAsync();

View file

@ -4,7 +4,6 @@
using Hosts.ServiceDefaults;
using Hosts.Tests.PageModels;
using Hosts.Tests.TestInfra;
using Xunit.Abstractions;
namespace Hosts.Tests;
@ -20,7 +19,7 @@ public class BffBlazorWebAssemblyTests(ITestOutputHelper output, AppHostFixture
};
}
[SkippableFact]
[Fact]
public async Task Can_login_and_load_local_api()
{
await Warmup();

View file

@ -3,7 +3,6 @@
using Hosts.ServiceDefaults;
using Hosts.Tests.TestInfra;
using Xunit.Abstractions;
namespace Hosts.Tests;
@ -18,14 +17,14 @@ public class BffTests : IntegrationTestBase
_bffClient = new BffClient(CreateHttpClient(AppHostServices.Bff));
}
[SkippableFact]
[Fact]
public async Task Can_invoke_home()
{
var response = await _httpClient.GetAsync("/");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
[SkippableFact]
[Fact]
public async Task Can_initiate_login()
{
@ -39,7 +38,7 @@ public class BffTests : IntegrationTestBase
claims.Any().ShouldBeTrue();
}
[SkippableTheory]
[Theory]
[InlineData("/local/self-contained")]
[InlineData("/local/invokes-external-api")]
[InlineData("/api/user-token")]
@ -55,7 +54,7 @@ public class BffTests : IntegrationTestBase
await _bffClient.InvokeApi(url);
}
[SkippableFact]
[Fact]
public async Task Can_logout()
{
await _bffClient.TriggerLogin();

View file

@ -4,7 +4,6 @@
using Hosts.ServiceDefaults;
using Hosts.Tests.PageModels;
using Hosts.Tests.TestInfra;
using Xunit.Abstractions;
namespace Hosts.Tests;
@ -21,7 +20,7 @@ public class BlazorPerComponentTests(ITestOutputHelper output, AppHostFixture fi
};
}
[SkippableFact]
[Fact]
public async Task Can_load_blazor_webassembly_app()
{
await Warmup();

View file

@ -13,30 +13,25 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" />
<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.TextWriter" />
<PackageReference Include="Serilog.Sinks.XUnit" />
<PackageReference Include="Serilog.Sinks.XUnit3" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="Microsoft.Playwright.Xunit" />
<PackageReference Include="Microsoft.Playwright.Xunit.v3" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' != 'Debug_NCrunch'">
<ProjectReference Include="..\..\hosts\Hosts.AppHost\Hosts.AppHost.csproj" />
<Using Include="Aspire.Hosting.ApplicationModel" />
<Using Include="Aspire.Hosting.Testing" />
</ItemGroup>
<ItemGroup>
<Using Include="System.Net" />
<Using Include="Aspire.Hosting.ApplicationModel" />
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="System.Net" />
<Using Include="Xunit" />
</ItemGroup>

View file

@ -4,9 +4,8 @@
using System.Reflection;
using Hosts.Tests.TestInfra;
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using Microsoft.Playwright.Xunit.v3;
using Xunit.v3;
namespace Hosts.Tests;
@ -31,12 +30,12 @@ public class PlaywrightTestBase : PageTest, IDisposable
#if DEBUG_NCRUNCH
// Running in NCrunch. NCrunch cannot build the aspire project, so it needs
// to be started manually.
Skip.If(true, "When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
Assert.Skip("When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
#endif
}
}
public override async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await base.InitializeAsync();
Context.SetDefaultTimeout(10_000);
@ -49,7 +48,7 @@ public class PlaywrightTestBase : PageTest, IDisposable
});
}
public override async Task DisposeAsync()
public override async ValueTask DisposeAsync()
{
var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Environment.CurrentDirectory;
// if path ends with /bin/{build configuration}/{dotnetversion}, then strip that from the path.
@ -106,18 +105,17 @@ public class PlaywrightTestBase : PageTest, IDisposable
public HttpClient CreateHttpClient(string clientName) => Fixture.CreateHttpClient(clientName);
}
public class WithTestNameAttribute : BeforeAfterTestAttribute
public class WithTestNameAttribute : Attribute, IBeforeAfterTestAttribute
{
public static string CurrentTestName = string.Empty;
public static string CurrentClassName = string.Empty;
public override void Before(MethodInfo methodInfo)
public void Before(MethodInfo methodInfo, IXunitTest _)
{
CurrentTestName = methodInfo.Name;
CurrentClassName = methodInfo.DeclaringType!.Name;
}
public override void After(MethodInfo methodInfo)
{
}
public void After(MethodInfo methodInfo, IXunitTest _)
{ }
}

View file

@ -51,7 +51,7 @@ public class AppHostFixture : IAsyncLifetime
public bool UsingAlreadyRunningInstance { get; private set; }
public string StartupLogs => _startupLogs.ToString() ?? string.Empty;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
using var startupLogWriter = ConnectLogger(s => _startupLogs.Write(s));
@ -130,7 +130,7 @@ public class AppHostFixture : IAsyncLifetime
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_app != null)
{
@ -272,7 +272,7 @@ public class AppHostFixture : IAsyncLifetime
return _app.GetEndpoint(clientName);
#else
Skip.If(true, "When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
Assert.Skip("When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
return null!;
#endif
}

View file

@ -1,7 +1,6 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Xunit.Abstractions;
namespace Hosts.Tests.TestInfra;
@ -24,7 +23,7 @@ public class IntegrationTestBase : IDisposable
#if DEBUG_NCRUNCH
// Running in NCrunch. NCrunch cannot build the aspire project, so it needs
// to be started manually.
Skip.If(true, "When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
Assert.Skip("When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
#endif
}
}

View file

@ -12,11 +12,9 @@
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.TextWriter" />
<PackageReference Include="Serilog.Sinks.XUnit" />
<PackageReference Include="Serilog.Sinks.XUnit3" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="Microsoft.Playwright.Xunit" />
</ItemGroup>
<ItemGroup>

View file

@ -5,7 +5,6 @@ using Duende.IdentityServer.EndToEndTests.TestInfra;
using Duende.Xunit.Playwright;
using Projects;
using ServiceDefaults;
using Xunit.Abstractions;
namespace Duende.IdentityServer.EndToEndTests;

View file

@ -3,7 +3,6 @@
using Duende.Xunit.Playwright;
using Projects;
using Xunit.Abstractions;
namespace Duende.IdentityServer.EndToEndTests.TestInfra;

View file

@ -2,6 +2,7 @@
// See LICENSE in the project root for license information.
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -50,5 +51,6 @@ public class IntegrationTest<TClass, TDbContext, TStoreOption> : IClassFixture<D
}
}
protected IntegrationTest(DatabaseProviderFixture<TDbContext> fixture) => fixture.Options = TestDatabaseProviders.ToList<DbContextOptions<TDbContext>>();
protected IntegrationTest(DatabaseProviderFixture<TDbContext> fixture)
=> fixture.Options = TestDatabaseProviders.Select(row => row.Data).ToList();
}

View file

@ -167,7 +167,7 @@ public class ClientStoreTests : IntegrationTest<ClientStoreTests, ConfigurationD
}
else
{
throw new TestTimeoutException(timeout);
TestTimeoutException.ForTimedOutTest(timeout);
}
}
}

View file

@ -90,7 +90,7 @@ public class DynamicProvidersTests
await ctx.SignOutAsync();
});
};
_idp1.InitializeAsync().Wait();
_idp1.InitializeAsync().AsTask().Wait();
_idp2 = new GenericHost("https://idp2");
_idp2.OnConfigureServices += services =>
@ -133,9 +133,7 @@ public class DynamicProvidersTests
await ctx.SignInAsync(new IdentityServerUser("2").CreatePrincipal());
});
};
_idp2.InitializeAsync().Wait();
_idp2.InitializeAsync().AsTask().Wait();
_host = new GenericHost("https://server");
_host.OnConfigureServices += services =>
@ -403,21 +401,15 @@ public class DynamicProvidersTests
public override string ToString() => Name;
}
private class DynamicProviderConfigurationData : TheoryData<DynamicProviderTestScenario>
private sealed class DynamicProviderConfigurationData : TheoryData<DynamicProviderTestScenario>
{
public DynamicProviderConfigurationData()
{
Add(new("Default PathPrefix", _ => { }));
Add(new("PathPrefix Callback",
options => options.DynamicProviders.PathMatchingCallback = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/federation/idp1", StringComparison.InvariantCulture))
{
return Task.FromResult("idp1");
}
return Task.FromResult((string)null);
}));
Add(new DynamicProviderTestScenario("Default PathPrefix", _ => { }));
Add(new DynamicProviderTestScenario("PathPrefix Callback",
options => options.DynamicProviders.PathMatchingCallback = ctx => ctx.Request.Path.StartsWithSegments("/federation/idp1", StringComparison.InvariantCulture)
? Task.FromResult("idp1")
: Task.FromResult((string)null)));
}
}
}

View file

@ -52,7 +52,7 @@ public class GenericHost
return _baseAddress + path;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{

View file

@ -15,9 +15,9 @@ public class ConfigurationIntegrationTestBase
{
var dbRoot = new InMemoryDatabaseRoot();
IdentityServerHost = new IdentityServerHost(dbRoot);
IdentityServerHost.InitializeAsync().Wait();
IdentityServerHost.InitializeAsync().AsTask().Wait();
ConfigurationHost = new ConfigurationHost(dbRoot);
ConfigurationHost.InitializeAsync().Wait();
ConfigurationHost.InitializeAsync().AsTask().Wait();
}
}

View file

@ -514,13 +514,13 @@ public class ClientConfigurationValidation
context.ErrorMessage.ShouldBe(expectedError);
}
public static TheoryData<ICollection<string>> GrantTypesWithClientCredentialsTestData =>
[
public static TheoryData<ICollection<string>> GrantTypesWithClientCredentialsTestData => new()
{
GrantTypes.ImplicitAndClientCredentials,
GrantTypes.CodeAndClientCredentials,
GrantTypes.HybridAndClientCredentials,
GrantTypes.ClientCredentials,
GrantTypes.ResourceOwnerPasswordAndClientCredentials
];
};
}

View file

@ -24,7 +24,7 @@ public class AppHostFixture<THost>(IAppHostServiceRoutes routes) : IAsyncLifetim
public bool UsingAlreadyRunningInstance { get; private set; }
public string StartupLogs => _startupLogs.ToString() ?? string.Empty;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
using var startupLogWriter = ConnectLogger(s => _startupLogs.Write(s));
@ -103,7 +103,7 @@ public class AppHostFixture<THost>(IAppHostServiceRoutes routes) : IAsyncLifetim
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_app != null)
{
@ -237,7 +237,7 @@ public class AppHostFixture<THost>(IAppHostServiceRoutes routes) : IAsyncLifetim
return _app.GetEndpoint(hostName);
#else
Skip.If(true, "When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
Assert.Skip("When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
return null!;
#endif
}

View file

@ -18,13 +18,12 @@
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.TextWriter" />
<PackageReference Include="Serilog.Sinks.XUnit" />
<PackageReference Include="Serilog.Sinks.XUnit3" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="Microsoft.Playwright.Xunit" />
<PackageReference Include="Microsoft.Playwright" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit.core" />
<PackageReference Include="xunit.v3.extensibility.core" />
</ItemGroup>
<ItemGroup>

View file

@ -1,7 +1,6 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Xunit.Abstractions;
namespace Duende.Xunit.Playwright;
@ -23,7 +22,7 @@ public class IntegrationTestBase<THost> : IDisposable where THost : class
#if DEBUG_NCRUNCH
// Running in NCrunch. NCrunch cannot build the aspire project, so it needs
// to be started manually.
Skip.If(true, "When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
Assert.Skip("When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
#endif
}
}

View file

@ -3,8 +3,6 @@
using System.Reflection;
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Duende.Xunit.Playwright;
@ -12,13 +10,20 @@ namespace Duende.Xunit.Playwright;
public class Defaults
{
public static readonly PageGotoOptions PageGotoOptions = new PageGotoOptions()
{ WaitUntil = WaitUntilState.NetworkIdle };
{
WaitUntil = WaitUntilState.NetworkIdle
};
}
[WithTestName]
public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : class
public class PlaywrightTestBase<THost> : IAsyncLifetime, IDisposable where THost : class
{
private readonly IDisposable _loggingScope;
private IPlaywright? _playwright;
private IBrowser? _browser;
protected IBrowserContext Context { get; private set; } = null!;
protected IPage Page { get; private set; } = null!;
public PlaywrightTestBase(ITestOutputHelper output, AppHostFixture<THost> fixture)
{
@ -35,14 +40,22 @@ public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : cla
#if DEBUG_NCRUNCH
// Running in NCrunch. NCrunch cannot build the aspire project, so it needs
// to be started manually.
Skip.If(true, "When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
Assert.Skip("When running the Host.Tests using NCrunch, you must start the Hosts.AppHost project manually. IE: dotnet run -p bff/samples/Hosts.AppHost. Or start without debugging from the UI. ");
#endif
}
}
public override async Task InitializeAsync()
public AppHostFixture<THost> Fixture { get; }
public ITestOutputHelper Output { get; }
public virtual async ValueTask InitializeAsync()
{
await base.InitializeAsync();
_playwright = await Microsoft.Playwright.Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync(new() { Headless = true });
Context = await _browser.NewContextAsync(ContextOptions());
Page = await Context.NewPageAsync();
Context.SetDefaultTimeout(10_000);
await Context.Tracing.StartAsync(new()
{
@ -53,7 +66,7 @@ public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : cla
});
}
public override async Task DisposeAsync()
public virtual async ValueTask DisposeAsync()
{
var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Environment.CurrentDirectory;
// if path ends with /bin/{build configuration}/{dotnetversion}, then strip that from the path.
@ -63,7 +76,6 @@ public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : cla
path = Path.GetFullPath(Path.Combine(path, "../../../"));
}
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
@ -72,10 +84,13 @@ public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : cla
$"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}.zip"
)
});
await base.DisposeAsync();
await Context.CloseAsync();
await _browser!.DisposeAsync();
_playwright!.Dispose();
}
public override BrowserNewContextOptions ContextOptions() => new()
public virtual BrowserNewContextOptions ContextOptions() => new()
{
Locale = "en-US",
ColorScheme = ColorScheme.Light,
@ -86,11 +101,6 @@ public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : cla
IgnoreHTTPSErrors = true,
};
public AppHostFixture<THost> Fixture { get; }
public ITestOutputHelper Output { get; }
public void Dispose()
{
if (!Fixture.UsingAlreadyRunningInstance)
@ -109,19 +119,3 @@ public class PlaywrightTestBase<THost> : PageTest, IDisposable where THost : cla
public HttpClient CreateHttpClient(string clientName) => Fixture.CreateHttpClient(clientName);
}
public class WithTestNameAttribute : BeforeAfterTestAttribute
{
public static string CurrentTestName = string.Empty;
public static string CurrentClassName = string.Empty;
public override void Before(MethodInfo methodInfo)
{
CurrentTestName = methodInfo.Name;
CurrentClassName = methodInfo.DeclaringType!.Name;
}
public override void After(MethodInfo methodInfo)
{
}
}

View file

@ -1,15 +0,0 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Xunit.Sdk;
namespace Duende.Xunit.Playwright.Retries;
[XunitTestCaseDiscoverer(
typeName: "Duende.Hosts.Tests.TestInfra.Retries.RetryableTestDiscoverer",
assemblyName: "Duende.Hosts.Tests"
)]
public class RetryableFact : FactAttribute
{
public int MaxRetries { get; set; } = 5;
}

View file

@ -1,61 +0,0 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Duende.Xunit.Playwright.Retries;
public class RetryableTestCase(
IMessageSink sink,
TestMethodDisplay display,
TestMethodDisplayOptions methodDisplayOptions,
ITestMethod method
) : XunitTestCase(sink,
display,
methodDisplayOptions,
method,
testMethodArguments: null)
{
public override async Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
object[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cts)
{
var retryCount = 0;
var maxRetries = Method.GetCustomAttributes(typeof(RetryableFact)).FirstOrDefault()?.GetNamedArgument<int>(nameof(RetryableFact.MaxRetries)) ?? 5;
while (true)
{
retryCount++;
var exceptionCapturingBus = new ExceptionCapturingMessageBus(messageBus);
var summary = await base.RunAsync(
diagnosticMessageSink,
exceptionCapturingBus,
constructorArguments,
aggregator,
cts);
summary.Failed -= exceptionCapturingBus.SkippedCount;
summary.Skipped += exceptionCapturingBus.SkippedCount;
if (aggregator.HasExceptions || summary.Failed == 0 || retryCount >= maxRetries)
{
exceptionCapturingBus.Flush();
return summary;
}
diagnosticMessageSink.OnMessage(new DiagnosticMessage(
"Execution of retriable test '{0}' failed. Attempt {1} of {2}",
DisplayName,
retryCount,
maxRetries
));
}
}
}

View file

@ -1,102 +0,0 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Duende.Xunit.Playwright.Retries;
public class RetryableTestDiscoverer(IMessageSink messageSink) : IXunitTestCaseDiscoverer
{
public IEnumerable<IXunitTestCase> Discover(
ITestFrameworkDiscoveryOptions discoveryOptions,
ITestMethod testMethod,
IAttributeInfo factAttribute)
{
yield return new RetryableTestCase(
messageSink,
discoveryOptions.MethodDisplayOrDefault(),
discoveryOptions.MethodDisplayOptionsOrDefault(),
testMethod
);
}
}
public class ExceptionCapturingMessageBus(IMessageBus inner) : IMessageBus
{
private readonly object _syncRoot = new();
private readonly Queue<IMessageSinkMessage> _failedMessages = new();
private bool _disposed = false;
public bool ExceptionHasOccurred { get; private set; }
public int SkippedCount { get; private set; }
public bool QueueMessage(IMessageSinkMessage message)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ExceptionCapturingMessageBus));
}
var skipTest = false;
if (message is ITestFailed failed)
{
// We ignore 'skip' exceptions
if (failed.ExceptionTypes.Contains("XUnit.SkipException", StringComparer.InvariantCultureIgnoreCase))
{
skipTest = true;
}
else
{
ExceptionHasOccurred = true;
}
if (skipTest)
{
SkippedCount++;
return inner.QueueMessage(new TestSkipped(failed.Test, "Skipped"));
}
}
lock (_syncRoot)
{
_failedMessages.Enqueue(message);
}
return true;
}
public void Flush()
{
lock (_syncRoot)
{
while (_failedMessages.Any())
{
inner.QueueMessage(_failedMessages.Dequeue());
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
lock (_syncRoot)
{
if (_disposed)
{
return;
}
_disposed = true;
}
Flush();
}
}

View file

@ -0,0 +1,22 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Reflection;
using Xunit.v3;
namespace Duende.Xunit.Playwright;
public class WithTestNameAttribute : Attribute, IBeforeAfterTestAttribute
{
public static string CurrentTestName = string.Empty;
public static string CurrentClassName = string.Empty;
public void Before(MethodInfo methodInfo, IXunitTest _)
{
CurrentTestName = methodInfo.Name;
CurrentClassName = methodInfo.DeclaringType!.Name;
}
public void After(MethodInfo methodInfo, IXunitTest _)
{ }
}

View file

@ -62,10 +62,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MinVer">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -6,6 +6,7 @@
<DebugType>full</DebugType>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<OutputType>Exe</OutputType>
<ImplicitUsings>true</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- <Nullable>enable</Nullable> -->
@ -25,17 +26,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit.core" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.v3.core" />
</ItemGroup>
<ItemGroup>