diff --git a/.editorconfig b/.editorconfig
index 8b6b66f0..90272961 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -156,4 +156,7 @@ csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
# CA1848: Use the LoggerMessage delegates
-dotnet_diagnostic.CA1848.severity = suggestion
\ No newline at end of file
+dotnet_diagnostic.CA1848.severity = suggestion
+# Test files - allow underscore-separated test method names (CA1707)
+[{EssentialCSharp.Web.Tests,EssentialCSharp.Chat.Tests}/**]
+dotnet_diagnostic.CA1707.severity = none
diff --git a/.github/workflows/PR-Build-And-Test.yml b/.github/workflows/PR-Build-And-Test.yml
index 1fd7a1d2..4117d976 100644
--- a/.github/workflows/PR-Build-And-Test.yml
+++ b/.github/workflows/PR-Build-And-Test.yml
@@ -35,7 +35,7 @@ jobs:
run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false
- name: Run .NET Tests
- run: dotnet test --no-build --configuration Release --logger trx --results-directory ${{ runner.temp }}
+ run: dotnet test --no-build --configuration Release --report-trx --coverage --results-directory ${{ runner.temp }}
- name: Convert TRX to VS Playlist
if: failure()
diff --git a/Directory.Packages.props b/Directory.Packages.props
index cacbdee9..5ae6f88a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -21,7 +21,7 @@
-
+
@@ -36,7 +36,6 @@
-
@@ -50,7 +49,5 @@
-
-
diff --git a/EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj b/EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj
index 62dc7206..c8638c29 100644
--- a/EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj
+++ b/EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj
@@ -6,19 +6,13 @@
-
-
-
-
+
-
-
-
diff --git a/EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs b/EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs
index 8aab8cb6..e14ef47e 100644
--- a/EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs
+++ b/EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs
@@ -2,13 +2,12 @@
using Moq;
namespace EssentialCSharp.Chat.Tests;
-// TODO: Move to editorconfig later, just moving quick
-#pragma warning disable CA1707 // Identifiers should not contain underscores
+
public class MarkdownChunkingServiceTests
{
#region MarkdownContentToHeadersAndSection
- [Fact]
- public void MarkdownContentToHeadersAndSection_ParsesSampleMarkdown_CorrectlyCombinesHeadersAndExtractsContent()
+ [Test]
+ public async Task MarkdownContentToHeadersAndSection_ParsesSampleMarkdown_CorrectlyCombinesHeadersAndExtractsContent()
{
string markdown = """
### Beginner Topic
@@ -43,15 +42,17 @@ publicstaticvoid Main() // Method declaration
var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
- Assert.Equal(3, sections.Count);
- Assert.Contains(sections, s => s.Header == "Beginner Topic: What Is a Method?" && string.Join("\n", s.Content).Contains("Syntactically, a **method** in C# is a named block of code"));
- Assert.Contains(sections, s => s.Header == "Main Method" && string.Join("\n", s.Content).Contains("The location where C# programs begin execution is the **Main method**, which begins with `static void Main()`")
- && string.Join("\n", s.Content).Contains("publicclass Program"));
- Assert.Contains(sections, s => s.Header == "Main Method: Advanced Topic: Declaration of the Main Method" && string.Join("\n", s.Content).Contains("C# requires that the Main method return either `void` or `int`"));
+ await Assert.That(sections.Count).IsEqualTo(3);
+ await Assert.That(sections).Contains(s => s.Header == "Beginner Topic: What Is a Method?" && string.Join("\n", s.Content).Contains("Syntactically, a **method** in C# is a named block of code"));
+
+ await Assert.That(sections).Contains(s => s.Header == "Main Method" && string.Join("\n", s.Content).Contains("The location where C# programs begin execution is the **Main method**, which begins with `static void Main()`"));
+ await Assert.That(sections).Contains(s => s.Header == "Main Method" && string.Join("\n", s.Content).Contains("publicclass Program"));
+
+ await Assert.That(sections).Contains(s => s.Header == "Main Method: Advanced Topic: Declaration of the Main Method" && string.Join("\n", s.Content).Contains("C# requires that the Main method return either `void` or `int`"));
}
- [Fact]
- public void MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
+ [Test]
+ public async Task MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
{
string markdown = """
## Working with Variables
@@ -86,16 +87,14 @@ publicstaticvoid Main()
var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
- Assert.Equal(2, sections.Count);
+ await Assert.That(sections.Count).IsEqualTo(2);
// The code listing should be appended to the Working with Variables section, not as its own section
- var workingWithVariablesSection = sections.FirstOrDefault(s => s.Header == "Working with Variables");
- Assert.True(!string.IsNullOrEmpty(workingWithVariablesSection.Header));
- Assert.Contains("publicclass MiracleMax", string.Join("\n", workingWithVariablesSection.Content));
- Assert.DoesNotContain(sections, s => s.Header == "Listing 1.12: Declaring and Assigning a Variable");
+ await Assert.That(sections).Contains(s => s.Header == "Working with Variables" && string.Join("\n", s.Content).Contains("publicclass MiracleMax"));
+ await Assert.That(sections).DoesNotContain(s => s.Header == "Listing 1.12: Declaring and Assigning a Variable");
}
- [Fact]
- public void MarkdownContentToHeadersAndSection_KeepsPriorHeadersAppended()
+ [Test]
+ public async Task MarkdownContentToHeadersAndSection_KeepsPriorHeadersAppended()
{
string markdown = """
### Beginner Topic
@@ -143,19 +142,23 @@ publicstaticvoid Main()
""";
var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
- Assert.Equal(5, sections.Count);
+ await Assert.That(sections.Count).IsEqualTo(5);
+
+ await Assert.That(sections).Contains(s => s.Header == "Beginner Topic: What Is a Data Type?" && string.Join("\n", s.Content).Contains("The type of data that a variable declaration specifies is called a **data type**"));
- Assert.Contains(sections, s => s.Header == "Beginner Topic: What Is a Data Type?" && string.Join("\n", s.Content).Contains("The type of data that a variable declaration specifies is called a **data type**"));
- Assert.Contains(sections, s => s.Header == "Declaring a Variable" && string.Join("\n", s.Content).Contains("In Listing 1.12, `string max` is a variable declaration"));
- Assert.Contains(sections, s => s.Header == "Declaring a Variable: Declaring another thing" && string.Join("\n", s.Content).Contains("Because a multivariable declaration statement allows developers to provide the data type only once"));
- Assert.Contains(sections, s => s.Header == "Assigning a Variable" && string.Join("\n", s.Content).Contains("After declaring a local variable, you must assign it a value before reading from it."));
- Assert.Contains(sections, s => s.Header == "Assigning a Variable: Continued Learning" && string.Join("\n", s.Content).Contains("From this listing, observe that it is possible to assign a variable as part of the variable declaration"));
+ await Assert.That(sections).Contains(s => s.Header == "Declaring a Variable" && string.Join("\n", s.Content).Contains("In Listing 1.12, `string max` is a variable declaration"));
+
+ await Assert.That(sections).Contains(s => s.Header == "Declaring a Variable: Declaring another thing" && string.Join("\n", s.Content).Contains("Because a multivariable declaration statement allows developers to provide the data type only once"));
+
+ await Assert.That(sections).Contains(s => s.Header == "Assigning a Variable" && string.Join("\n", s.Content).Contains("After declaring a local variable, you must assign it a value before reading from it."));
+
+ await Assert.That(sections).Contains(s => s.Header == "Assigning a Variable: Continued Learning" && string.Join("\n", s.Content).Contains("From this listing, observe that it is possible to assign a variable as part of the variable declaration"));
}
#endregion MarkdownContentToHeadersAndSection
#region ProcessSingleMarkdownFile
- [Fact]
- public void ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
+ [Test]
+ public async Task ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
{
// Arrange
var logger = new Mock>().Object;
@@ -178,15 +181,12 @@ public void ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
var result = service.ProcessSingleMarkdownFile(fileContent, fileName, filePath);
// Assert
- Assert.NotNull(result);
- Assert.Equal(fileName, result.FileName);
- Assert.Equal(filePath, result.FilePath);
- Assert.Contains("This is the first section.", string.Join("\n", result.Chunks));
- Assert.Contains("Console.WriteLine(\"Hello World\");", string.Join("\n", result.Chunks));
- Assert.Contains("This is the second section.", string.Join("\n", result.Chunks));
- Assert.Contains(result.Chunks, c => c.Contains("This is the second section."));
+ await Assert.That(result).IsNotNull();
+ await Assert.That(result.FileName).IsEqualTo(fileName);
+ await Assert.That(result.FilePath).IsEqualTo(filePath);
+ await Assert.That(string.Join("\n", result.Chunks)).Contains("This is the first section.");
+ await Assert.That(string.Join("\n", result.Chunks)).Contains("Console.WriteLine(\"Hello World\");");
+ await Assert.That(result.Chunks).Contains(c => c.Contains("This is the second section."));
}
#endregion ProcessSingleMarkdownFile
}
-
-#pragma warning restore CA1707 // Identifiers should not contain underscores
diff --git a/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj b/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj
index 88e40aa3..50848f91 100644
--- a/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj
+++ b/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj
@@ -5,27 +5,14 @@
false
false
-
- $(NoWarn);CA1707
-
+
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
diff --git a/EssentialCSharp.Web.Tests/FunctionalTests.cs b/EssentialCSharp.Web.Tests/FunctionalTests.cs
index abdf499d..78edc27a 100644
--- a/EssentialCSharp.Web.Tests/FunctionalTests.cs
+++ b/EssentialCSharp.Web.Tests/FunctionalTests.cs
@@ -2,57 +2,53 @@
namespace EssentialCSharp.Web.Tests;
-public class FunctionalTests
+[NotInParallel("FunctionalTests")]
+[ClassDataSource(Shared = SharedType.PerClass)]
+public class FunctionalTests(WebApplicationFactory factory)
{
- [Theory]
- [InlineData("/")]
- [InlineData("/hello-world")]
- [InlineData("/hello-world#hello-world")]
- [InlineData("/guidelines")]
- [InlineData("/healthz")]
+ [Test]
+ [Arguments("/")]
+ [Arguments("/hello-world")]
+ [Arguments("/hello-world#hello-world")]
+ [Arguments("/guidelines")]
+ [Arguments("/healthz")]
public async Task WhenTheApplicationStarts_ItCanLoadLoadPages(string relativeUrl)
{
- using WebApplicationFactory factory = new();
-
HttpClient client = factory.CreateClient();
using HttpResponseMessage response = await client.GetAsync(relativeUrl);
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
}
- [Theory]
- [InlineData("/guidelines?rid=test-referral-id")]
- [InlineData("/about?rid=abc123")]
- [InlineData("/hello-world?rid=user-referral")]
- [InlineData("/guidelines?rid=")]
- [InlineData("/about?rid= ")]
- [InlineData("/guidelines?foo=bar")]
- [InlineData("/about?someOtherParam=value")]
+ [Test]
+ [Arguments("/guidelines?rid=test-referral-id")]
+ [Arguments("/about?rid=abc123")]
+ [Arguments("/hello-world?rid=user-referral")]
+ [Arguments("/guidelines?rid=")]
+ [Arguments("/about?rid= ")]
+ [Arguments("/guidelines?foo=bar")]
+ [Arguments("/about?someOtherParam=value")]
public async Task WhenPagesAreAccessed_TheyReturnHtml(string relativeUrl)
{
- using WebApplicationFactory factory = new();
-
HttpClient client = factory.CreateClient();
using HttpResponseMessage response = await client.GetAsync(relativeUrl);
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
-
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
// Ensure the response has content (not blank)
string content = await response.Content.ReadAsStringAsync();
- Assert.NotEmpty(content);
-
+ await Assert.That(content).IsNotEmpty();
+
// Verify it's actually HTML content, not just whitespace
- Assert.Contains("
-{
- [Fact]
- public async Task CaptchaService_Verify_Success()
- {
- ICaptchaService captchaService = serviceProvider.ServiceProvider.GetRequiredService();
-
- // From https://docs.hcaptcha.com/#integration-testing-test-keys
- string hCaptchaSecret = "0x0000000000000000000000000000000000000000";
- string hCaptchaToken = "10000000-aaaa-bbbb-cccc-000000000001";
- string hCaptchaSiteKey = "10000000-ffff-ffff-ffff-000000000001";
- HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey);
-
- Assert.NotNull(response);
- Assert.True(response.Success);
- }
-}
-
-public class CaptchaServiceProvider
-{
- public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
- public static ServiceProvider CreateServiceProvider()
- {
- IServiceCollection services = new ServiceCollection();
-
- IConfigurationRoot configuration = new ConfigurationBuilder()
- .SetBasePath(IntelliTect.Multitool.RepositoryPaths.GetDefaultRepoRoot())
- .AddJsonFile($"{nameof(EssentialCSharp)}.{nameof(Web)}/appsettings.json")
- .Build();
- services.AddCaptchaService(configuration.GetSection(CaptchaOptions.CaptchaSender));
- // Add other necessary services here
-
- return services.BuildServiceProvider();
- }
-}
+namespace EssentialCSharp.Web.Extensions.Tests.Integration;
+
+[ClassDataSource(Shared = SharedType.PerClass)]
+public class CaptchaTests(CaptchaServiceProvider serviceProvider)
+{
+ [Test]
+ [Timeout(30_000)]
+ public async Task CaptchaService_Verify_Success(CancellationToken cancellationToken)
+ {
+ ICaptchaService captchaService = serviceProvider.ServiceProvider.GetRequiredService();
+
+ // From https://docs.hcaptcha.com/#integration-testing-test-keys
+ string hCaptchaSecret = "0x0000000000000000000000000000000000000000";
+ string hCaptchaToken = "10000000-aaaa-bbbb-cccc-000000000001";
+ string hCaptchaSiteKey = "10000000-ffff-ffff-ffff-000000000001";
+ HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey);
+
+ await Assert.That(response).IsNotNull();
+ await Assert.That(response.Success).IsTrue();
+ }
+}
+
+public class CaptchaServiceProvider : IDisposable
+{
+ public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
+ public static ServiceProvider CreateServiceProvider()
+ {
+ IServiceCollection services = new ServiceCollection();
+
+ IConfigurationRoot configuration = new ConfigurationBuilder()
+ .SetBasePath(IntelliTect.Multitool.RepositoryPaths.GetDefaultRepoRoot())
+ .AddJsonFile($"{nameof(EssentialCSharp)}.{nameof(Web)}/appsettings.json")
+ .Build();
+ services.AddCaptchaService(configuration.GetSection(CaptchaOptions.CaptchaSender));
+ // Add other necessary services here
+
+ return services.BuildServiceProvider();
+ }
+ public void Dispose()
+ {
+ ServiceProvider.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/ListingSourceCodeControllerTests.cs b/EssentialCSharp.Web.Tests/ListingSourceCodeControllerTests.cs
index 1944d309..6eda7819 100644
--- a/EssentialCSharp.Web.Tests/ListingSourceCodeControllerTests.cs
+++ b/EssentialCSharp.Web.Tests/ListingSourceCodeControllerTests.cs
@@ -4,104 +4,112 @@
namespace EssentialCSharp.Web.Tests;
-public class ListingSourceCodeControllerTests
+[ClassDataSource(Shared = SharedType.PerClass)]
+public class ListingSourceCodeControllerTests(WebApplicationFactory factory)
{
- [Fact]
+ [Test]
public async Task GetListing_WithValidChapterAndListing_Returns200WithContent()
{
// Arrange
- using WebApplicationFactory factory = new();
HttpClient client = factory.CreateClient();
// Act
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/1/listing/1");
// Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
-
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
ListingSourceCodeResponse? result = await response.Content.ReadFromJsonAsync();
- Assert.NotNull(result);
- Assert.Equal(1, result.ChapterNumber);
- Assert.Equal(1, result.ListingNumber);
- Assert.NotEmpty(result.FileExtension);
- Assert.NotEmpty(result.Content);
+ await Assert.That(result).IsNotNull();
+ using (Assert.Multiple())
+ {
+ await Assert.That(result.ChapterNumber).IsEqualTo(1);
+ await Assert.That(result.ListingNumber).IsEqualTo(1);
+ await Assert.That(result.FileExtension).IsNotEmpty();
+ await Assert.That(result.Content).IsNotEmpty();
+ }
}
- [Fact]
+ [Test]
public async Task GetListing_WithInvalidChapter_Returns404()
{
// Arrange
- using WebApplicationFactory factory = new();
HttpClient client = factory.CreateClient();
// Act
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/999/listing/1");
// Assert
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
}
- [Fact]
+ [Test]
public async Task GetListing_WithInvalidListing_Returns404()
{
// Arrange
- using WebApplicationFactory factory = new();
HttpClient client = factory.CreateClient();
// Act
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/1/listing/999");
// Assert
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
}
- [Fact]
+ [Test]
public async Task GetListingsByChapter_WithValidChapter_ReturnsMultipleListings()
{
// Arrange
- using WebApplicationFactory factory = new();
HttpClient client = factory.CreateClient();
// Act
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/1");
// Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
-
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
List? results = await response.Content.ReadFromJsonAsync>();
- Assert.NotNull(results);
- Assert.NotEmpty(results);
-
+ await Assert.That(results).IsNotNull();
+ await Assert.That(results).IsNotEmpty();
+
// Verify all results are from chapter 1
- Assert.All(results, r => Assert.Equal(1, r.ChapterNumber));
-
+ foreach (var r in results)
+ {
+ using (Assert.Multiple())
+ {
+ await Assert.That(r.ChapterNumber).IsEqualTo(1);
+ }
+ }
+
// Verify results are ordered by listing number
- Assert.Equal(results.OrderBy(r => r.ListingNumber).ToList(), results);
-
+ await Assert.That(results).IsOrderedBy(r => r.ListingNumber);
+
// Verify each listing has required properties
- Assert.All(results, r =>
+ foreach (var r in results)
{
- Assert.NotEmpty(r.FileExtension);
- Assert.NotEmpty(r.Content);
- });
+ using (Assert.Multiple())
+ {
+ await Assert.That(r.FileExtension).IsNotEmpty();
+ await Assert.That(r.Content).IsNotEmpty();
+ }
+ }
}
- [Fact]
+ [Test]
public async Task GetListingsByChapter_WithInvalidChapter_ReturnsEmptyList()
{
// Arrange
- using WebApplicationFactory factory = new();
HttpClient client = factory.CreateClient();
// Act
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/999");
// Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
-
+ await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
List? results = await response.Content.ReadFromJsonAsync>();
- Assert.NotNull(results);
- Assert.Empty(results);
+ await Assert.That(results).IsNotNull();
+ await Assert.That(results).IsEmpty();
}
-}
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs b/EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs
index fbc1d595..fd781e03 100644
--- a/EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs
+++ b/EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs
@@ -9,7 +9,7 @@ namespace EssentialCSharp.Web.Tests;
public class ListingSourceCodeServiceTests
{
- [Fact]
+ [Test]
public async Task GetListingAsync_WithValidChapterAndListing_ReturnsCorrectListing()
{
// Arrange
@@ -19,14 +19,17 @@ public async Task GetListingAsync_WithValidChapterAndListing_ReturnsCorrectListi
ListingSourceCodeResponse? result = await service.GetListingAsync(1, 1);
// Assert
- Assert.NotNull(result);
- Assert.Equal(1, result.ChapterNumber);
- Assert.Equal(1, result.ListingNumber);
- Assert.Equal("cs", result.FileExtension);
- Assert.NotEmpty(result.Content);
+ await Assert.That(result).IsNotNull();
+ using (Assert.Multiple())
+ {
+ await Assert.That(result.ChapterNumber).IsEqualTo(1);
+ await Assert.That(result.ListingNumber).IsEqualTo(1);
+ await Assert.That(result.FileExtension).IsEqualTo("cs");
+ await Assert.That(result.Content).IsNotEmpty();
+ }
}
- [Fact]
+ [Test]
public async Task GetListingAsync_WithInvalidChapter_ReturnsNull()
{
// Arrange
@@ -36,10 +39,10 @@ public async Task GetListingAsync_WithInvalidChapter_ReturnsNull()
ListingSourceCodeResponse? result = await service.GetListingAsync(999, 1);
// Assert
- Assert.Null(result);
+ await Assert.That(result).IsNull();
}
- [Fact]
+ [Test]
public async Task GetListingAsync_WithInvalidListing_ReturnsNull()
{
// Arrange
@@ -49,10 +52,10 @@ public async Task GetListingAsync_WithInvalidListing_ReturnsNull()
ListingSourceCodeResponse? result = await service.GetListingAsync(1, 999);
// Assert
- Assert.Null(result);
+ await Assert.That(result).IsNull();
}
- [Fact]
+ [Test]
public async Task GetListingAsync_DifferentFileExtension_AutoDiscoversFileExtension()
{
// Arrange
@@ -62,11 +65,11 @@ public async Task GetListingAsync_DifferentFileExtension_AutoDiscoversFileExtens
ListingSourceCodeResponse? result = await service.GetListingAsync(1, 2);
// Assert
- Assert.NotNull(result);
- Assert.Equal("xml", result.FileExtension);
+ await Assert.That(result).IsNotNull();
+ await Assert.That(result.FileExtension).IsEqualTo("xml");
}
- [Fact]
+ [Test]
public async Task GetListingsByChapterAsync_WithValidChapter_ReturnsAllListings()
{
// Arrange
@@ -76,16 +79,22 @@ public async Task GetListingsByChapterAsync_WithValidChapter_ReturnsAllListings(
IReadOnlyList results = await service.GetListingsByChapterAsync(1);
// Assert
- Assert.NotEmpty(results);
- Assert.All(results, r => Assert.Equal(1, r.ChapterNumber));
- Assert.All(results, r => Assert.NotEmpty(r.Content));
- Assert.All(results, r => Assert.NotEmpty(r.FileExtension));
-
+ await Assert.That(results).IsNotEmpty();
+ foreach (var r in results)
+ {
+ using (Assert.Multiple())
+ {
+ await Assert.That(r.ChapterNumber).IsEqualTo(1);
+ await Assert.That(r.Content).IsNotEmpty();
+ await Assert.That(r.FileExtension).IsNotEmpty();
+ }
+ }
+
// Verify results are ordered
- Assert.Equal(results.OrderBy(r => r.ListingNumber).ToList(), results);
+ await Assert.That(results).IsOrderedBy(r => r.ListingNumber);
}
- [Fact]
+ [Test]
public async Task GetListingsByChapterAsync_DirectoryContainsNonListingFiles_ExcludesNonListingFiles()
{
// Arrange - Chapter 10 has Employee.cs which doesn't match the pattern
@@ -95,17 +104,20 @@ public async Task GetListingsByChapterAsync_DirectoryContainsNonListingFiles_Exc
IReadOnlyList results = await service.GetListingsByChapterAsync(10);
// Assert
- Assert.NotEmpty(results);
-
+ await Assert.That(results).IsNotEmpty();
+
// Ensure all results match the {CC}.{LL}.{ext} pattern
- Assert.All(results, r =>
+ foreach (var r in results)
{
- Assert.Equal(10, r.ChapterNumber);
- Assert.InRange(r.ListingNumber, 1, 99);
- });
+ using (Assert.Multiple())
+ {
+ await Assert.That(r.ChapterNumber).IsEqualTo(10);
+ await Assert.That(r.ListingNumber).IsBetween(1, 99);
+ }
+ }
}
- [Fact]
+ [Test]
public async Task GetListingsByChapterAsync_WithInvalidChapter_ReturnsEmptyList()
{
// Arrange
@@ -115,7 +127,7 @@ public async Task GetListingsByChapterAsync_WithInvalidChapter_ReturnsEmptyList(
IReadOnlyList results = await service.GetListingsByChapterAsync(999);
// Assert
- Assert.Empty(results);
+ await Assert.That(results).IsEmpty();
}
private static ListingSourceCodeService CreateService()
@@ -144,4 +156,4 @@ private static DirectoryInfo GetTestDataPath()
return testDataDirectory;
}
-}
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs b/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs
index 9280e3fd..ed5866fc 100644
--- a/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs
+++ b/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs
@@ -3,7 +3,8 @@
namespace EssentialCSharp.Web.Tests;
-public class RouteConfigurationServiceTests : IClassFixture
+[ClassDataSource(Shared = SharedType.PerClass)]
+public class RouteConfigurationServiceTests
{
private readonly WebApplicationFactory _Factory;
@@ -12,8 +13,8 @@ public RouteConfigurationServiceTests(WebApplicationFactory factory)
_Factory = factory;
}
- [Fact]
- public void GetStaticRoutes_ShouldReturnExpectedRoutes()
+ [Test]
+ public async Task GetStaticRoutes_ShouldReturnExpectedRoutes()
{
// Act
var routes = _Factory.InServiceScope(serviceProvider =>
@@ -23,13 +24,13 @@ public void GetStaticRoutes_ShouldReturnExpectedRoutes()
});
// Assert
- Assert.NotEmpty(routes);
+ await Assert.That(routes).IsNotEmpty();
// Check for expected routes from the HomeController
- Assert.Contains("home", routes);
- Assert.Contains("about", routes);
- Assert.Contains("guidelines", routes);
- Assert.Contains("announcements", routes);
- Assert.Contains("termsofservice", routes);
+ await Assert.That(routes).Contains("home");
+ await Assert.That(routes).Contains("about");
+ await Assert.That(routes).Contains("guidelines");
+ await Assert.That(routes).Contains("announcements");
+ await Assert.That(routes).Contains("termsofservice");
}
-}
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/SiteMappingTests.cs b/EssentialCSharp.Web.Tests/SiteMappingTests.cs
index cf4c2c34..91b4e7d9 100644
--- a/EssentialCSharp.Web.Tests/SiteMappingTests.cs
+++ b/EssentialCSharp.Web.Tests/SiteMappingTests.cs
@@ -51,40 +51,40 @@ public static List GetSiteMap()
];
}
- [Fact]
- public void FindHelloWorldWithAnchorSlugReturnsCorrectSiteMap()
+ [Test]
+ public async Task FindHelloWorldWithAnchorSlugReturnsCorrectSiteMap()
{
SiteMapping? foundSiteMap = GetSiteMap().Find("hello-world#hello-world");
- Assert.NotNull(foundSiteMap);
- Assert.Equivalent(HelloWorldSiteMapping, foundSiteMap);
+ await Assert.That(foundSiteMap).IsNotNull();
+ await Assert.That(foundSiteMap).IsEquivalentTo(HelloWorldSiteMapping);
}
- [Fact]
- public void FindCSyntaxFundamentalsWithSpacesReturnsCorrectSiteMap()
+ [Test]
+ public async Task FindCSyntaxFundamentalsWithSpacesReturnsCorrectSiteMap()
{
SiteMapping? foundSiteMap = GetSiteMap().Find("C# Syntax Fundamentals");
- Assert.NotNull(foundSiteMap);
- Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap);
+ await Assert.That(foundSiteMap).IsNotNull();
+ await Assert.That(foundSiteMap).IsEquivalentTo(CSyntaxFundamentalsSiteMapping);
}
- [Fact]
- public void FindCSyntaxFundamentalsWithSpacesAndAnchorReturnsCorrectSiteMap()
+ [Test]
+ public async Task FindCSyntaxFundamentalsWithSpacesAndAnchorReturnsCorrectSiteMap()
{
SiteMapping? foundSiteMap = GetSiteMap().Find("C# Syntax Fundamentals#hello-world");
- Assert.NotNull(foundSiteMap);
- Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap);
+ await Assert.That(foundSiteMap).IsNotNull();
+ await Assert.That(foundSiteMap).IsEquivalentTo(CSyntaxFundamentalsSiteMapping);
}
- [Fact]
- public void FindCSyntaxFundamentalsSanitizedWithAnchorReturnsCorrectSiteMap()
+ [Test]
+ public async Task FindCSyntaxFundamentalsSanitizedWithAnchorReturnsCorrectSiteMap()
{
SiteMapping? foundSiteMap = GetSiteMap().Find("c-syntax-fundamentals#hello-world");
- Assert.NotNull(foundSiteMap);
- Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap);
+ await Assert.That(foundSiteMap).IsNotNull();
+ await Assert.That(foundSiteMap).IsEquivalentTo(CSyntaxFundamentalsSiteMapping);
}
- [Fact]
- public void FindPercentComplete_KeyIsNull_ReturnsNull()
+ [Test]
+ public async Task FindPercentComplete_KeyIsNull_ReturnsNull()
{
// Arrange
@@ -92,29 +92,26 @@ public void FindPercentComplete_KeyIsNull_ReturnsNull()
string? percent = GetSiteMap().FindPercentComplete(null!);
// Assert
- Assert.Null(percent);
+ await Assert.That(percent).IsNull();
}
- [Theory]
- [InlineData(" ")]
- [InlineData("")]
- public void FindPercentComplete_KeyIsWhiteSpace_ThrowsArgumentException(string? key)
+ [Test]
+ [Arguments(" ")]
+ [Arguments("")]
+ public async Task FindPercentComplete_KeyIsWhiteSpace_ThrowsArgumentException(string? key)
{
// Arrange
// Act
// Assert
- Assert.Throws(() =>
- {
- GetSiteMap().FindPercentComplete(key);
- });
+ await Assert.That(() => GetSiteMap().FindPercentComplete(key)).Throws();
}
- [Theory]
- [InlineData("hello-world", "50.00")]
- [InlineData("c-syntax-fundamentals", "100.00")]
- public void FindPercentComplete_ValidKey_Success(string? key, string result)
+ [Test]
+ [Arguments("hello-world", "50.00")]
+ [Arguments("c-syntax-fundamentals", "100.00")]
+ public async Task FindPercentComplete_ValidKey_Success(string? key, string result)
{
// Arrange
@@ -122,11 +119,11 @@ public void FindPercentComplete_ValidKey_Success(string? key, string result)
string? percent = GetSiteMap().FindPercentComplete(key);
// Assert
- Assert.Equal(result, percent);
+ await Assert.That(percent).IsEqualTo(result);
}
- [Fact]
- public void FindPercentComplete_EmptySiteMappings_ReturnsZeroPercent()
+ [Test]
+ public async Task FindPercentComplete_EmptySiteMappings_ReturnsZeroPercent()
{
// Arrange
IList siteMappings = new List();
@@ -135,11 +132,11 @@ public void FindPercentComplete_EmptySiteMappings_ReturnsZeroPercent()
string? percent = siteMappings.FindPercentComplete("test");
// Assert
- Assert.Equal("0.00", percent);
+ await Assert.That(percent).IsEqualTo("0.00");
}
- [Fact]
- public void FindPercentComplete_KeyNotFound_ReturnsZeroPercent()
+ [Test]
+ public async Task FindPercentComplete_KeyNotFound_ReturnsZeroPercent()
{
// Arrange
@@ -147,6 +144,6 @@ public void FindPercentComplete_KeyNotFound_ReturnsZeroPercent()
string? percent = GetSiteMap().FindPercentComplete("non-existent-key");
// Assert
- Assert.Equal("0.00", percent);
+ await Assert.That(percent).IsEqualTo("0.00");
}
-}
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs b/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs
index be8edc90..ba9e029a 100644
--- a/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs
+++ b/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs
@@ -1,13 +1,15 @@
+using System.IO;
using System.Globalization;
using DotnetSitemapGenerator;
using EssentialCSharp.Web.Helpers;
using EssentialCSharp.Web.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-
namespace EssentialCSharp.Web.Tests;
-public class SitemapXmlHelpersTests : IClassFixture
+[NotInParallel("SitemapTests")]
+[ClassDataSource(Shared = SharedType.PerClass)]
+public class SitemapXmlHelpersTests
{
private readonly WebApplicationFactory _Factory;
@@ -16,8 +18,8 @@ public SitemapXmlHelpersTests(WebApplicationFactory factory)
_Factory = factory;
}
- [Fact]
- public void EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow()
+ [Test]
+ public async Task EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow()
{
// Arrange
var siteMappings = new List
@@ -28,12 +30,11 @@ public void EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow()
};
// Act & Assert
- var exception = Record.Exception(() => SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings));
- Assert.Null(exception);
+ await Assert.That(() => SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings)).ThrowsNothing();
}
- [Fact]
- public void EnsureSitemapHealthy_WithMultipleCanonicalLinksForSamePage_ThrowsException()
+ [Test]
+ public async Task EnsureSitemapHealthy_WithMultipleCanonicalLinksForSamePage_ThrowsException()
{
// Arrange - Two mappings for the same chapter/page both marked as canonical
var siteMappings = new List
@@ -43,15 +44,14 @@ public void EnsureSitemapHealthy_WithMultipleCanonicalLinksForSamePage_ThrowsExc
};
// Act & Assert
- var exception = Assert.Throws(() =>
- SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings));
-
- Assert.Contains("Chapter 1, Page 1", exception.Message);
- Assert.Contains("more than one canonical link", exception.Message);
+ await Assert.That(() => SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings))
+ .Throws()
+ .WithMessageContaining("Chapter 1, Page 1")
+ .And.HasMessageContaining("more than one canonical link");
}
- [Fact]
- public void EnsureSitemapHealthy_WithNoCanonicalLinksForPage_ThrowsException()
+ [Test]
+ public async Task EnsureSitemapHealthy_WithNoCanonicalLinksForPage_ThrowsException()
{
// Arrange - No mappings marked as canonical for this page
var siteMappings = new List
@@ -61,14 +61,13 @@ public void EnsureSitemapHealthy_WithNoCanonicalLinksForPage_ThrowsException()
};
// Act & Assert
- var exception = Assert.Throws(() =>
- SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings));
-
- Assert.Contains("Chapter 1, Page 1", exception.Message);
+ await Assert.That(() => SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings))
+ .Throws()
+ .WithMessageContaining("Chapter 1, Page 1");
}
- [Fact]
- public void GenerateSitemapXml_DoesNotIncludeIdentityRoutes()
+ [Test]
+ public async Task GenerateSitemapXml_DoesNotIncludeIdentityRoutes()
{
// Arrange
var tempDir = new DirectoryInfo(Path.GetTempPath());
@@ -87,16 +86,16 @@ public void GenerateSitemapXml_DoesNotIncludeIdentityRoutes()
var allUrls = nodes.Select(n => n.Url).ToList();
// Verify no Identity routes are included
- Assert.DoesNotContain(allUrls, url => url.Contains("Identity", StringComparison.OrdinalIgnoreCase));
- Assert.DoesNotContain(allUrls, url => url.Contains("Account", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).DoesNotContain(url => url.Contains("Identity", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).DoesNotContain(url => url.Contains("Account", StringComparison.OrdinalIgnoreCase));
// But verify that expected routes are included
- Assert.Contains(allUrls, url => url.Contains("/home", StringComparison.OrdinalIgnoreCase));
- Assert.Contains(allUrls, url => url.Contains("/about", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).Contains(url => url.Contains("/home", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).Contains(url => url.Contains("/about", StringComparison.OrdinalIgnoreCase));
}
- [Fact]
- public void GenerateSitemapXml_IncludesBaseUrl()
+ [Test]
+ public async Task GenerateSitemapXml_IncludesBaseUrl()
{
// Arrange
var tempDir = new DirectoryInfo(Path.GetTempPath());
@@ -112,16 +111,19 @@ public void GenerateSitemapXml_IncludesBaseUrl()
baseUrl,
out var nodes);
- Assert.Contains(nodes, node => node.Url == baseUrl);
+ await Assert.That(nodes).Contains(node => node.Url == baseUrl);
// Verify the root URL has highest priority
var rootNode = nodes.First(node => node.Url == baseUrl);
- Assert.Equal(1.0M, rootNode.Priority);
- Assert.Equal(ChangeFrequency.Daily, rootNode.ChangeFrequency);
+ using (Assert.Multiple())
+ {
+ await Assert.That(rootNode.Priority).IsEqualTo(1.0M);
+ await Assert.That(rootNode.ChangeFrequency).IsEqualTo(ChangeFrequency.Daily);
+ }
}
- [Fact]
- public void GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
+ [Test]
+ public async Task GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
{
// Arrange
var tempDir = new DirectoryInfo(Path.GetTempPath());
@@ -145,13 +147,13 @@ public void GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
var allUrls = nodes.Select(n => n.Url).ToList();
- Assert.Contains(allUrls, url => url.Contains("test-page-1"));
- Assert.DoesNotContain(allUrls, url => url.Contains("test-page-2")); // Not marked for XML
- Assert.Contains(allUrls, url => url.Contains("test-page-3"));
+ await Assert.That(allUrls).Contains(url => url.Contains("test-page-1", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).DoesNotContain(url => url.Contains("test-page-2", StringComparison.OrdinalIgnoreCase)); // Not marked for XML
+ await Assert.That(allUrls).Contains(url => url.Contains("test-page-3", StringComparison.OrdinalIgnoreCase));
}
- [Fact]
- public void GenerateSitemapXml_DoesNotIncludeIndexRoutes()
+ [Test]
+ public async Task GenerateSitemapXml_DoesNotIncludeIndexRoutes()
{
// Arrange
var tempDir = new DirectoryInfo(Path.GetTempPath());
@@ -170,11 +172,11 @@ public void GenerateSitemapXml_DoesNotIncludeIndexRoutes()
var allUrls = nodes.Select(n => n.Url).ToList();
// Should not include Index action routes (they're the default)
- Assert.DoesNotContain(allUrls, url => url.Contains("/Index", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).DoesNotContain(url => url.Contains("/Index", StringComparison.OrdinalIgnoreCase));
}
- [Fact]
- public void GenerateSitemapXml_DoesNotIncludeErrorRoutes()
+ [Test]
+ public async Task GenerateSitemapXml_DoesNotIncludeErrorRoutes()
{
// Arrange
var tempDir = new DirectoryInfo(Path.GetTempPath());
@@ -193,11 +195,11 @@ public void GenerateSitemapXml_DoesNotIncludeErrorRoutes()
var allUrls = nodes.Select(n => n.Url).ToList();
// Should not include Error action routes
- Assert.DoesNotContain(allUrls, url => url.Contains("/Error", StringComparison.OrdinalIgnoreCase));
+ await Assert.That(allUrls).DoesNotContain(url => url.Contains("/Error", StringComparison.OrdinalIgnoreCase));
}
- [Fact]
- public void GenerateSitemapXml_UsesLastModifiedDateFromSiteMapping()
+ [Test]
+ public async Task GenerateSitemapXml_UsesLastModifiedDateFromSiteMapping()
{
// Arrange
var tempDir = new DirectoryInfo(Path.GetTempPath());
@@ -220,7 +222,7 @@ public void GenerateSitemapXml_UsesLastModifiedDateFromSiteMapping()
// Assert
var siteMappingNode = nodes.First(node => node.Url.Contains("test-page-1"));
- Assert.Equal(specificLastModified, siteMappingNode.LastModificationDate);
+ await Assert.That(siteMappingNode.LastModificationDate).IsEqualTo(specificLastModified);
}
private static SiteMapping CreateSiteMapping(
@@ -246,4 +248,4 @@ private static SiteMapping CreateSiteMapping(
lastModified: lastModified
);
}
-}
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/StringExtensionsTests.cs b/EssentialCSharp.Web.Tests/StringExtensionsTests.cs
index 50116ab3..254db0d2 100644
--- a/EssentialCSharp.Web.Tests/StringExtensionsTests.cs
+++ b/EssentialCSharp.Web.Tests/StringExtensionsTests.cs
@@ -2,33 +2,33 @@
public class StringExtensionsTests
{
- [Theory]
- [InlineData(" ExtraSpacing ", "extraspacing")]
- [InlineData("Hello World", "hello-world")]
- [InlineData("Coding the Publish–Subscribe Pattern with Multicast Delegates", "coding-the-publish-subscribe-pattern-with-multicast-delegates")]
- [InlineData("C#", "c")]
- [InlineData("C# Syntax Fundamentals", "c-syntax-fundamentals")]
- [InlineData("C#_Syntax_Fundamentals", "c-syntax-fundamentals")]
- [InlineData("C# Syntax_Fundamentals-for-me", "c-syntax-fundamentals-for-me")]
- [InlineData("Bitwise Operators (<<, >>, |, &, ^, ~)", "bitwise-operators")]
- [InlineData(".NET Standard", "net-standard")]
- [InlineData("Working with System.Threading", "working-with-system-threading")]
- public void SanitizeStringToOnlyHaveDashesAndLowerCase(string actual, string sanitized)
+ [Test]
+ [Arguments(" ExtraSpacing ", "extraspacing")]
+ [Arguments("Hello World", "hello-world")]
+ [Arguments("Coding the Publish–Subscribe Pattern with Multicast Delegates", "coding-the-publish-subscribe-pattern-with-multicast-delegates")]
+ [Arguments("C#", "c")]
+ [Arguments("C# Syntax Fundamentals", "c-syntax-fundamentals")]
+ [Arguments("C#_Syntax_Fundamentals", "c-syntax-fundamentals")]
+ [Arguments("C# Syntax_Fundamentals-for-me", "c-syntax-fundamentals-for-me")]
+ [Arguments("Bitwise Operators (<<, >>, |, &, ^, ~)", "bitwise-operators")]
+ [Arguments(".NET Standard", "net-standard")]
+ [Arguments("Working with System.Threading", "working-with-system-threading")]
+ public async Task SanitizeStringToOnlyHaveDashesAndLowerCase(string actual, string sanitized)
{
- Assert.Equal(sanitized, actual.Sanitize());
- Assert.Equal(sanitized, actual.Sanitize().Sanitize());
+ await Assert.That(actual.Sanitize()).IsEqualTo(sanitized);
+ await Assert.That(actual.Sanitize().Sanitize()).IsEqualTo(sanitized);
}
- [Theory]
- [InlineData("hello-world#hello-world", "hello-world")]
- [InlineData("C#Syntax#hello-world", "csyntax")]
- [InlineData("C#Syntax", "csyntax")]
- [InlineData("cSyntax", "csyntax")]
- [InlineData(".NET", "net")]
- [InlineData("System.Threading", "system-threading")]
- public void GetPotentialMatches(string actual, string match)
+ [Test]
+ [Arguments("hello-world#hello-world", "hello-world")]
+ [Arguments("C#Syntax#hello-world", "csyntax")]
+ [Arguments("C#Syntax", "csyntax")]
+ [Arguments("cSyntax", "csyntax")]
+ [Arguments(".NET", "net")]
+ [Arguments("System.Threading", "system-threading")]
+ public async Task GetPotentialMatches(string actual, string match)
{
var matches = actual.GetPotentialMatches().ToList();
- Assert.Contains(match, matches);
+ await Assert.That(matches).Contains(match);
}
-}
+}
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/Usings.cs b/EssentialCSharp.Web.Tests/Usings.cs
index 8c927eb7..e69de29b 100644
--- a/EssentialCSharp.Web.Tests/Usings.cs
+++ b/EssentialCSharp.Web.Tests/Usings.cs
@@ -1 +0,0 @@
-global using Xunit;
\ No newline at end of file
diff --git a/EssentialCSharp.Web.Tests/WebApplicationFactory.cs b/EssentialCSharp.Web.Tests/WebApplicationFactory.cs
index 07e60731..70bc647a 100644
--- a/EssentialCSharp.Web.Tests/WebApplicationFactory.cs
+++ b/EssentialCSharp.Web.Tests/WebApplicationFactory.cs
@@ -1,6 +1,7 @@
using System.Data.Common;
using EssentialCSharp.Web.Data;
using EssentialCSharp.Web.Services;
+using TUnit.Core.Interfaces;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
@@ -13,8 +14,17 @@
namespace EssentialCSharp.Web.Tests;
-public sealed class WebApplicationFactory : WebApplicationFactory
+public sealed class WebApplicationFactory : WebApplicationFactory, IAsyncInitializer
{
+ public Task InitializeAsync()
+ {
+ // Force eager server initialization before tests run.
+ // This is thread-safe and prevents race conditions from parallel tests
+ // calling CreateClient() concurrently during lazy init.
+ _ = Server;
+ return Task.CompletedTask;
+ }
+
private static string SqlConnectionString => $"DataSource=file:{Guid.NewGuid()}?mode=memory&cache=shared";
private SqliteConnection? _Connection;
diff --git a/global.json b/global.json
index d50dabe4..7e5a4f3c 100644
--- a/global.json
+++ b/global.json
@@ -2,5 +2,8 @@
"sdk": {
"version": "10.0.103",
"rollForward": "latestMinor"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
}
}
\ No newline at end of file