feat: Migrate tests from xunit to TUnit#875
Conversation
BenjaminMichaelis
commented
Feb 25, 2026
- Replace xunit + coverlet with TUnit test framework
- Update test project files to use TUnit package references
- Add Microsoft.Testing.Platform runner config to global.json
- Update CI test command for TUnit MTP compatibility
- Convert all test classes and assertions to TUnit syntax
- Add [NotInParallel] to tests using shared SQLite state
- Use IsEquivalentTo for collection comparisons
- Replace xunit + coverlet with TUnit test framework - Update test project files to use TUnit package references - Add Microsoft.Testing.Platform runner config to global.json - Update CI test command for TUnit MTP compatibility - Convert all test classes and assertions to TUnit syntax - Add [NotInParallel] to tests using shared SQLite state - Use IsEquivalentTo for collection comparisons Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Migrates the solution’s test projects from xUnit/vstest to TUnit running on Microsoft.Testing.Platform (MTP), updating CI and test code to the new attribute/assertion model.
Changes:
- Configure
dotnet testto use Microsoft.Testing.Platform viaglobal.json, and update PR CI to emit TRX under MTP. - Replace xUnit + coverlet + vstest package references with TUnit in both test projects.
- Convert test attributes and assertions from xUnit syntax to TUnit syntax (including parallelization controls/data sources).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| global.json | Selects Microsoft.Testing.Platform as the test runner for dotnet test. |
| EssentialCSharp.Web.Tests/Usings.cs | Removes global xUnit using. |
| EssentialCSharp.Web.Tests/StringExtensionsTests.cs | Converts xUnit theory/assertions to TUnit [Test]/[Arguments] + Assert.That. |
| EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs | Converts fixture pattern to TUnit data source and adds [NotInParallel]. |
| EssentialCSharp.Web.Tests/SiteMappingTests.cs | Converts xUnit facts/theories to TUnit tests and equivalents. |
| EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs | Converts xUnit fixture usage to TUnit class data source. |
| EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs | Converts assertions to TUnit and adjusts collection comparisons. |
| EssentialCSharp.Web.Tests/ListingSourceCodeControllerTests.cs | Converts integration tests to TUnit assertions. |
| EssentialCSharp.Web.Tests/Integration/CaptchaTests.cs | Converts xUnit fixture test to TUnit class data source and assertions. |
| EssentialCSharp.Web.Tests/FunctionalTests.cs | Converts xUnit theories/facts to TUnit argument-based tests. |
| EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj | Swaps xUnit/vstest/coverlet references for TUnit. |
| EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs | Converts xUnit tests to TUnit assertions. |
| EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj | Swaps xUnit/vstest/coverlet references for TUnit. |
| Directory.Packages.props | Central package updates: remove xUnit/vstest/coverlet versions; add TUnit version. |
| .github/workflows/PR-Build-And-Test.yml | Updates dotnet test invocation to use MTP TRX reporting arguments. |
| [Fact] | ||
| public void EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow() | ||
| [Test] | ||
| public async Task EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow() |
There was a problem hiding this comment.
EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow is declared as async Task but contains no await. With TreatWarningsAsErrors enabled, this will fail the build with CS1998. Remove async (or return Task.CompletedTask), or add an awaited TUnit assertion (e.g., an explicit does-not-throw assertion).
| public async Task EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow() | |
| public void EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow() |
| 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(); |
There was a problem hiding this comment.
result is nullable (ListingSourceCodeResponse?) but is dereferenced after IsNotNull(). If TUnit assertions don’t update nullable flow analysis, result.ChapterNumber will raise CS8602 and fail the build under TreatWarningsAsErrors. Consider using result! (or a non-null local) before accessing properties.
| 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(); | |
| ListingSourceCodeResponse nonNullResult = result!; | |
| await Assert.That(nonNullResult.ChapterNumber).IsEqualTo(1); | |
| await Assert.That(nonNullResult.ListingNumber).IsEqualTo(1); | |
| await Assert.That(nonNullResult.FileExtension).IsEqualTo("cs"); | |
| await Assert.That(nonNullResult.Content).IsNotEmpty(); |
| // Assert | ||
| Assert.NotNull(result); | ||
| Assert.Equal("xml", result.FileExtension); | ||
| await Assert.That(result).IsNotNull(); | ||
| await Assert.That(result.FileExtension).IsEqualTo("xml"); |
There was a problem hiding this comment.
Same nullable-flow issue here: result is nullable and result.FileExtension is accessed after IsNotNull(). If TUnit doesn’t inform nullable analysis, this will produce CS8602 with warnings-as-errors. Use result! (or capture a non-null local) before dereferencing.
| ListingSourceCodeResponse? result = await response.Content.ReadFromJsonAsync<ListingSourceCodeResponse>(); | ||
| 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(); | ||
| await Assert.That(result.ChapterNumber).IsEqualTo(1); | ||
| await Assert.That(result.ListingNumber).IsEqualTo(1); |
There was a problem hiding this comment.
result is nullable (ListingSourceCodeResponse?) but is dereferenced after IsNotNull(). If the TUnit assertion doesn’t affect nullable analysis, accessing result.ChapterNumber/etc. will emit CS8602 and fail the build with warnings-as-errors. Use result! (or a non-null local) before accessing properties.
| List<ListingSourceCodeResponse>? results = await response.Content.ReadFromJsonAsync<List<ListingSourceCodeResponse>>(); | ||
| 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) | ||
| { |
There was a problem hiding this comment.
results is declared nullable but is iterated/used after IsNotNull(). If TUnit’s IsNotNull() doesn’t update nullable flow, foreach (var r in results) will trigger CS8602 under TreatWarningsAsErrors. Consider assigning var nonNullResults = results!; after the assertion and using that for the rest of the test.
| HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey); | ||
|
|
||
| await Assert.That(response).IsNotNull(); | ||
| await Assert.That(response.Success).IsTrue(); |
There was a problem hiding this comment.
response is HCaptchaResult? and is dereferenced (response.Success) after IsNotNull(). If TUnit’s IsNotNull() doesn’t inform nullable flow analysis, this will produce CS8602 and fail the build with warnings-as-errors. Use response! (or capture a non-null local) before accessing .Success.
| await Assert.That(response.Success).IsTrue(); | |
| HCaptchaResult nonNullResponse = response!; | |
| await Assert.That(nonNullResponse.Success).IsTrue(); |