Skip to content

Commit 5189994

Browse files
feat: Migrate tests from xunit to TUnit
- 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>
1 parent 7e6678d commit 5189994

15 files changed

Lines changed: 293 additions & 295 deletions

.github/workflows/PR-Build-And-Test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false
3636

3737
- name: Run .NET Tests
38-
run: dotnet test --no-build --configuration Release --logger trx --results-directory ${{ runner.temp }}
38+
run: dotnet test --no-build --configuration Release -- --report-trx --results-directory ${{ runner.temp }}
3939

4040
- name: Convert TRX to VS Playlist
4141
if: failure()

Directory.Packages.props

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
2222
<PackageVersion Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.4.0" />
2323
<PackageVersion Include="Microsoft.ApplicationInsights.Profiler.AspNetCore" Version="3.0.1" />
24-
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
24+
<PackageVersion Include="TUnit" Version="1.17.11" />
2525
<PackageVersion Include="EssentialCSharp.Shared.Models" Version="$(ToolingPackagesVersion)" />
2626
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
2727
<PackageVersion Include="IntelliTect.Multitool" Version="1.5.3" />
@@ -36,7 +36,6 @@
3636
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
3737
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
3838
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
39-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
4039
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.72.0" />
4140
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.PgVector" Version="1.70.0-preview" />
4241
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103" />
@@ -50,7 +49,5 @@
5049
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
5150
<PackageVersion Include="Octokit" Version="14.0.0" />
5251
<PackageVersion Include="DotnetSitemapGenerator" Version="2.0.0" />
53-
<PackageVersion Include="xunit" Version="2.9.3" />
54-
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
5552
</ItemGroup>
5653
</Project>

EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,13 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="coverlet.collector" />
10-
<PackageReference Include="Microsoft.NET.Test.Sdk" />
119
<PackageReference Include="Moq" />
12-
<PackageReference Include="xunit" />
13-
<PackageReference Include="xunit.runner.visualstudio" />
10+
<PackageReference Include="TUnit" />
1411
</ItemGroup>
1512

1613
<ItemGroup>
1714
<ProjectReference Include="..\EssentialCSharp.Chat\EssentialCSharp.Chat.csproj" />
1815
</ItemGroup>
1916

20-
<ItemGroup>
21-
<Using Include="Xunit" />
22-
</ItemGroup>
2317

2418
</Project>

EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ namespace EssentialCSharp.Chat.Tests;
77
public class MarkdownChunkingServiceTests
88
{
99
#region MarkdownContentToHeadersAndSection
10-
[Fact]
11-
public void MarkdownContentToHeadersAndSection_ParsesSampleMarkdown_CorrectlyCombinesHeadersAndExtractsContent()
10+
[Test]
11+
public async Task MarkdownContentToHeadersAndSection_ParsesSampleMarkdown_CorrectlyCombinesHeadersAndExtractsContent()
1212
{
1313
string markdown = """
1414
### Beginner Topic
@@ -43,15 +43,15 @@ publicstaticvoid Main() // Method declaration
4343

4444
var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
4545

46-
Assert.Equal(3, sections.Count);
47-
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"));
48-
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()`")
46+
await Assert.That(sections.Count).IsEqualTo(3);
47+
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"));
48+
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()`")
4949
&& string.Join("\n", s.Content).Contains("publicclass Program"));
50-
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`"));
50+
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`"));
5151
}
5252

53-
[Fact]
54-
public void MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
53+
[Test]
54+
public async Task MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
5555
{
5656
string markdown = """
5757
## Working with Variables
@@ -86,16 +86,16 @@ publicstaticvoid Main()
8686

8787
var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
8888

89-
Assert.Equal(2, sections.Count);
89+
await Assert.That(sections.Count).IsEqualTo(2);
9090
// The code listing should be appended to the Working with Variables section, not as its own section
9191
var workingWithVariablesSection = sections.FirstOrDefault(s => s.Header == "Working with Variables");
92-
Assert.True(!string.IsNullOrEmpty(workingWithVariablesSection.Header));
93-
Assert.Contains("publicclass MiracleMax", string.Join("\n", workingWithVariablesSection.Content));
94-
Assert.DoesNotContain(sections, s => s.Header == "Listing 1.12: Declaring and Assigning a Variable");
92+
await Assert.That(!string.IsNullOrEmpty(workingWithVariablesSection.Header)).IsTrue();
93+
await Assert.That(string.Join("\n", workingWithVariablesSection.Content)).Contains("publicclass MiracleMax");
94+
await Assert.That(sections).DoesNotContain(s => s.Header == "Listing 1.12: Declaring and Assigning a Variable");
9595
}
9696

97-
[Fact]
98-
public void MarkdownContentToHeadersAndSection_KeepsPriorHeadersAppended()
97+
[Test]
98+
public async Task MarkdownContentToHeadersAndSection_KeepsPriorHeadersAppended()
9999
{
100100
string markdown = """
101101
### Beginner Topic
@@ -143,19 +143,19 @@ publicstaticvoid Main()
143143
""";
144144

145145
var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
146-
Assert.Equal(5, sections.Count);
146+
await Assert.That(sections.Count).IsEqualTo(5);
147147

148-
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**"));
149-
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"));
150-
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"));
151-
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."));
152-
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"));
148+
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**"));
149+
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"));
150+
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"));
151+
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."));
152+
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"));
153153
}
154154
#endregion MarkdownContentToHeadersAndSection
155155

156156
#region ProcessSingleMarkdownFile
157-
[Fact]
158-
public void ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
157+
[Test]
158+
public async Task ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
159159
{
160160
// Arrange
161161
var logger = new Mock<Microsoft.Extensions.Logging.ILogger<MarkdownChunkingService>>().Object;
@@ -178,13 +178,13 @@ public void ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
178178
var result = service.ProcessSingleMarkdownFile(fileContent, fileName, filePath);
179179

180180
// Assert
181-
Assert.NotNull(result);
182-
Assert.Equal(fileName, result.FileName);
183-
Assert.Equal(filePath, result.FilePath);
184-
Assert.Contains("This is the first section.", string.Join("\n", result.Chunks));
185-
Assert.Contains("Console.WriteLine(\"Hello World\");", string.Join("\n", result.Chunks));
186-
Assert.Contains("This is the second section.", string.Join("\n", result.Chunks));
187-
Assert.Contains(result.Chunks, c => c.Contains("This is the second section."));
181+
await Assert.That(result).IsNotNull();
182+
await Assert.That(result.FileName).IsEqualTo(fileName);
183+
await Assert.That(result.FilePath).IsEqualTo(filePath);
184+
await Assert.That(string.Join("\n", result.Chunks)).Contains("This is the first section.");
185+
await Assert.That(string.Join("\n", result.Chunks)).Contains("Console.WriteLine(\"Hello World\");");
186+
await Assert.That(string.Join("\n", result.Chunks)).Contains("This is the second section.");
187+
await Assert.That(result.Chunks).Contains(c => c.Contains("This is the second section."));
188188
}
189189
#endregion ProcessSingleMarkdownFile
190190
}

EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,9 @@
1414
<ItemGroup>
1515
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
1616
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
17-
<PackageReference Include="Microsoft.NET.Test.Sdk" />
1817
<PackageReference Include="Moq.AutoMock" />
18+
<PackageReference Include="TUnit" />
1919
<PackageReference Include="Newtonsoft.Json" />
20-
<PackageReference Include="xunit" />
21-
<PackageReference Include="xunit.runner.visualstudio">
22-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23-
<PrivateAssets>all</PrivateAssets>
24-
</PackageReference>
25-
<PackageReference Include="coverlet.collector">
26-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
27-
<PrivateAssets>all</PrivateAssets>
28-
</PackageReference>
2920
</ItemGroup>
3021

3122
<ItemGroup>
Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,59 @@
11
using System.Net;
2+
using System.Threading.Tasks;
23

34
namespace EssentialCSharp.Web.Tests;
45

56
public class FunctionalTests
67
{
7-
[Theory]
8-
[InlineData("/")]
9-
[InlineData("/hello-world")]
10-
[InlineData("/hello-world#hello-world")]
11-
[InlineData("/guidelines")]
12-
[InlineData("/healthz")]
8+
[Test]
9+
[Arguments("/")]
10+
[Arguments("/hello-world")]
11+
[Arguments("/hello-world#hello-world")]
12+
[Arguments("/guidelines")]
13+
[Arguments("/healthz")]
1314
public async Task WhenTheApplicationStarts_ItCanLoadLoadPages(string relativeUrl)
1415
{
1516
using WebApplicationFactory factory = new();
1617

1718
HttpClient client = factory.CreateClient();
1819
using HttpResponseMessage response = await client.GetAsync(relativeUrl);
1920

20-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
21+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
2122
}
2223

23-
[Theory]
24-
[InlineData("/guidelines?rid=test-referral-id")]
25-
[InlineData("/about?rid=abc123")]
26-
[InlineData("/hello-world?rid=user-referral")]
27-
[InlineData("/guidelines?rid=")]
28-
[InlineData("/about?rid= ")]
29-
[InlineData("/guidelines?foo=bar")]
30-
[InlineData("/about?someOtherParam=value")]
24+
[Test]
25+
[Arguments("/guidelines?rid=test-referral-id")]
26+
[Arguments("/about?rid=abc123")]
27+
[Arguments("/hello-world?rid=user-referral")]
28+
[Arguments("/guidelines?rid=")]
29+
[Arguments("/about?rid= ")]
30+
[Arguments("/guidelines?foo=bar")]
31+
[Arguments("/about?someOtherParam=value")]
3132
public async Task WhenPagesAreAccessed_TheyReturnHtml(string relativeUrl)
3233
{
3334
using WebApplicationFactory factory = new();
3435

3536
HttpClient client = factory.CreateClient();
3637
using HttpResponseMessage response = await client.GetAsync(relativeUrl);
3738

38-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
39-
39+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
40+
4041
// Ensure the response has content (not blank)
4142
string content = await response.Content.ReadAsStringAsync();
42-
Assert.NotEmpty(content);
43-
43+
await Assert.That(content).IsNotEmpty();
44+
4445
// Verify it's actually HTML content, not just whitespace
45-
Assert.Contains("<html", content, StringComparison.OrdinalIgnoreCase);
46+
await Assert.That(content).Contains("<html", StringComparison.OrdinalIgnoreCase);
4647
}
4748

48-
[Fact]
49+
[Test]
4950
public async Task WhenTheApplicationStarts_NonExistingPage_GivesCorrectStatusCode()
5051
{
5152
using WebApplicationFactory factory = new();
5253

5354
HttpClient client = factory.CreateClient();
5455
using HttpResponseMessage response = await client.GetAsync("/non-existing-page1234");
5556

56-
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
57+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
5758
}
58-
}
59+
}
Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,44 @@
1-
using EssentialCSharp.Web.Models;
2-
using EssentialCSharp.Web.Services;
3-
using Microsoft.Extensions.Configuration;
4-
using Microsoft.Extensions.DependencyInjection;
1+
using EssentialCSharp.Web.Models;
2+
using EssentialCSharp.Web.Services;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using System.Threading.Tasks;
56

6-
namespace EssentialCSharp.Web.Extensions.Tests.Integration;
7-
8-
public class CaptchaTests(CaptchaServiceProvider serviceProvider) : IClassFixture<CaptchaServiceProvider>
9-
{
10-
[Fact]
11-
public async Task CaptchaService_Verify_Success()
12-
{
13-
ICaptchaService captchaService = serviceProvider.ServiceProvider.GetRequiredService<ICaptchaService>();
14-
15-
// From https://docs.hcaptcha.com/#integration-testing-test-keys
16-
string hCaptchaSecret = "0x0000000000000000000000000000000000000000";
17-
string hCaptchaToken = "10000000-aaaa-bbbb-cccc-000000000001";
18-
string hCaptchaSiteKey = "10000000-ffff-ffff-ffff-000000000001";
19-
HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey);
20-
21-
Assert.NotNull(response);
22-
Assert.True(response.Success);
23-
}
24-
}
25-
26-
public class CaptchaServiceProvider
27-
{
28-
public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
29-
public static ServiceProvider CreateServiceProvider()
30-
{
31-
IServiceCollection services = new ServiceCollection();
32-
33-
IConfigurationRoot configuration = new ConfigurationBuilder()
34-
.SetBasePath(IntelliTect.Multitool.RepositoryPaths.GetDefaultRepoRoot())
35-
.AddJsonFile($"{nameof(EssentialCSharp)}.{nameof(Web)}/appsettings.json")
36-
.Build();
37-
services.AddCaptchaService(configuration.GetSection(CaptchaOptions.CaptchaSender));
38-
// Add other necessary services here
39-
40-
return services.BuildServiceProvider();
41-
}
42-
}
7+
namespace EssentialCSharp.Web.Extensions.Tests.Integration;
8+
9+
[ClassDataSource<CaptchaServiceProvider>(Shared = SharedType.PerClass)]
10+
public class CaptchaTests(CaptchaServiceProvider serviceProvider)
11+
{
12+
[Test]
13+
public async Task CaptchaService_Verify_Success()
14+
{
15+
ICaptchaService captchaService = serviceProvider.ServiceProvider.GetRequiredService<ICaptchaService>();
16+
17+
// From https://docs.hcaptcha.com/#integration-testing-test-keys
18+
string hCaptchaSecret = "0x0000000000000000000000000000000000000000";
19+
string hCaptchaToken = "10000000-aaaa-bbbb-cccc-000000000001";
20+
string hCaptchaSiteKey = "10000000-ffff-ffff-ffff-000000000001";
21+
HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey);
22+
23+
await Assert.That(response).IsNotNull();
24+
await Assert.That(response.Success).IsTrue();
25+
}
26+
}
27+
28+
public class CaptchaServiceProvider
29+
{
30+
public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
31+
public static ServiceProvider CreateServiceProvider()
32+
{
33+
IServiceCollection services = new ServiceCollection();
34+
35+
IConfigurationRoot configuration = new ConfigurationBuilder()
36+
.SetBasePath(IntelliTect.Multitool.RepositoryPaths.GetDefaultRepoRoot())
37+
.AddJsonFile($"{nameof(EssentialCSharp)}.{nameof(Web)}/appsettings.json")
38+
.Build();
39+
services.AddCaptchaService(configuration.GetSection(CaptchaOptions.CaptchaSender));
40+
// Add other necessary services here
41+
42+
return services.BuildServiceProvider();
43+
}
44+
}

0 commit comments

Comments
 (0)