Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .agents/skills/tunit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
description: Write or troubleshoot tests using TUnit, TUnit.Assertions, or TUnit.Mocks, or migrate tests or mocks to TUnit or TUnit.Mocks. Routes to task-specific official documentation. Use only for tasks involving these packages or migration to them.
license: MIT
metadata:
github-path: skills/tunit
github-ref: refs/tags/v1.75.0
github-repo: https://github.com/thomhurst/TUnit
github-tree-sha: 0d594ad16156140ff224e389ac441149d7259d62
name: tunit
---
# TUnit

Use this skill to find the TUnit guidance needed for the current task. Fetch documentation with an available web tool or HTTP client; network access is required. The documentation contains the API details and examples; do not load the whole site.

## Load only what the task needs

1. Check the relevant project files for TUnit package versions, target frameworks, and the SDK/test runner in `global.json`. Include centrally managed package versions when present. Preserve the project's versions unless an upgrade is requested or necessary and explained.
2. Choose the closest topic below and fetch that Markdown page before choosing APIs or commands. Start with one page; fetch another only when the task needs it. Skip pages already available in the conversation unless they need refreshing.
3. For an unlisted topic or a broken route, fetch [the documentation index](https://tunit.dev/llms.txt), search it for the feature or diagnostic, then fetch only the matching page. Do not fetch `llms-full.txt` or crawl every link.
4. Follow related links only to resolve a specific gap. Prefer the `.md` version: remove a trailing slash and append `.md` to an HTML documentation path, before any fragment. Keep existing `.md` paths unchanged. Resolve relative links against the fetched page's URL.
5. Apply the relevant example to the existing project and run a focused build or test when making code changes. Cite the documentation used. If current documentation differs from the installed API, check version-matched source or local API evidence before proposing an upgrade. If fetching fails, use available local documentation and state what could not be verified; do not guess APIs.

## Topic routes

Choose the link that matches the task; these are alternatives, not a reading checklist.

| Task | Documentation |
| --- | --- |
| Create or configure a project | [Installation](https://tunit.dev/docs/getting-started/installation.md) |
| Write a basic test | [First test](https://tunit.dev/docs/getting-started/writing-your-first-test.md) |
| Assertions, exceptions, or assertion composition | [Assertions](https://tunit.dev/docs/assertions/getting-started.md) |
| Parameterized tests | [Arguments](https://tunit.dev/docs/writing-tests/arguments.md), [method data](https://tunit.dev/docs/writing-tests/method-data-source.md), or [matrix data](https://tunit.dev/docs/writing-tests/matrix-tests.md) |
| Setup, teardown, or hooks | [Lifecycle](https://tunit.dev/docs/writing-tests/lifecycle.md) |
| Fixtures, shared resources, or dependency injection | [Class data sources](https://tunit.dev/docs/writing-tests/class-data-source.md) or [dependency injection](https://tunit.dev/docs/writing-tests/dependency-injection.md) |
| Concurrency or dependencies between tests | [Parallelism](https://tunit.dev/docs/execution/parallelism.md) or [ordering](https://tunit.dev/docs/writing-tests/ordering.md) |
| Run tests or select tests | [Running tests](https://tunit.dev/docs/getting-started/running-your-tests.md) or [filters](https://tunit.dev/docs/execution/test-filters.md) |
| Migrate an existing suite | [xUnit](https://tunit.dev/docs/migration/xunit.md), [NUnit](https://tunit.dev/docs/migration/nunit.md), or [MSTest](https://tunit.dev/docs/migration/mstest.md), matching the source framework |
| Mocking | [TUnit.Mocks](https://tunit.dev/docs/writing-tests/mocking.md) |
| Migrate mocks to TUnit.Mocks | [Moq](https://tunit.dev/docs/migration/mocking/moq.md), [NSubstitute](https://tunit.dev/docs/migration/mocking/nsubstitute.md), or [FakeItEasy](https://tunit.dev/docs/migration/mocking/fakeiteasy.md), matching the source library |
| Aspire integration testing | [Aspire](https://tunit.dev/docs/examples/aspire.md) |
| ASP.NET Core integration testing | [ASP.NET Core](https://tunit.dev/docs/examples/aspnet.md) |
| Playwright browser testing | [Playwright](https://tunit.dev/docs/examples/playwright.md) |
| Testcontainers and container fixtures | [ASP.NET Core integration examples](https://tunit.dev/docs/examples/aspnet.md) (see "With Testcontainers") |
| Native AOT, trimming, or reflection mode | [AOT compatibility](https://tunit.dev/docs/writing-tests/aot.md) or [engine modes](https://tunit.dev/docs/execution/engine-modes.md) |
| Custom assertions | [Assertion source generators](https://tunit.dev/docs/assertions/extensibility/source-generator-assertions.md) or [custom assertion implementations](https://tunit.dev/docs/assertions/extensibility/custom-assertions.md) |
| Custom data sources | [Data source generators](https://tunit.dev/docs/extending/data-source-generators.md) |
| Event receivers | [Event subscribing](https://tunit.dev/docs/writing-tests/event-subscribing.md) |
| TestContext and test metadata | [Test context](https://tunit.dev/docs/writing-tests/test-context.md) |
| Diagnostics, output, or tracing | [Logging](https://tunit.dev/docs/extending/logging.md), [test artifacts](https://tunit.dev/docs/writing-tests/artifacts.md), or [OpenTelemetry](https://tunit.dev/docs/examples/opentelemetry.md) |
| Cancellation | [Cancelling a test](https://tunit.dev/docs/execution/cancellation.md) |
| Discovery, build, or execution failures | [Troubleshooting](https://tunit.dev/docs/troubleshooting.md) |

For other topics, such as analyzers or framework-specific integrations, use the index selectively.

## Essential distinctions

- Await TUnit assertions, for example `await Assert.That(actual).IsEqualTo(expected);`.
- Tests run in parallel by default. Account for shared mutable state and resource lifetimes.
- TUnit uses Microsoft.Testing.Platform and `--treenode-filter`. Match command syntax to the project's SDK and runner mode; do not substitute VSTest `--filter` or assume every `dotnet test` invocation needs a `--` separator.
- Source-generated discovery is the default; reflection mode also exists. Check the relevant documentation when changing discovery or Native AOT behavior.
- TUnit.Assertions and TUnit.Mocks also work with other test frameworks. Using them alone does not require changing the project's test runner.
61 changes: 61 additions & 0 deletions .claude/skills/tunit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
description: Write or troubleshoot tests using TUnit, TUnit.Assertions, or TUnit.Mocks, or migrate tests or mocks to TUnit or TUnit.Mocks. Routes to task-specific official documentation. Use only for tasks involving these packages or migration to them.
license: MIT
metadata:
github-path: skills/tunit
github-ref: refs/tags/v1.75.0
github-repo: https://github.com/thomhurst/TUnit
github-tree-sha: 0d594ad16156140ff224e389ac441149d7259d62
name: tunit
---
# TUnit

Use this skill to find the TUnit guidance needed for the current task. Fetch documentation with an available web tool or HTTP client; network access is required. The documentation contains the API details and examples; do not load the whole site.

## Load only what the task needs

1. Check the relevant project files for TUnit package versions, target frameworks, and the SDK/test runner in `global.json`. Include centrally managed package versions when present. Preserve the project's versions unless an upgrade is requested or necessary and explained.
2. Choose the closest topic below and fetch that Markdown page before choosing APIs or commands. Start with one page; fetch another only when the task needs it. Skip pages already available in the conversation unless they need refreshing.
3. For an unlisted topic or a broken route, fetch [the documentation index](https://tunit.dev/llms.txt), search it for the feature or diagnostic, then fetch only the matching page. Do not fetch `llms-full.txt` or crawl every link.
4. Follow related links only to resolve a specific gap. Prefer the `.md` version: remove a trailing slash and append `.md` to an HTML documentation path, before any fragment. Keep existing `.md` paths unchanged. Resolve relative links against the fetched page's URL.
5. Apply the relevant example to the existing project and run a focused build or test when making code changes. Cite the documentation used. If current documentation differs from the installed API, check version-matched source or local API evidence before proposing an upgrade. If fetching fails, use available local documentation and state what could not be verified; do not guess APIs.

## Topic routes

Choose the link that matches the task; these are alternatives, not a reading checklist.

| Task | Documentation |
| --- | --- |
| Create or configure a project | [Installation](https://tunit.dev/docs/getting-started/installation.md) |
| Write a basic test | [First test](https://tunit.dev/docs/getting-started/writing-your-first-test.md) |
| Assertions, exceptions, or assertion composition | [Assertions](https://tunit.dev/docs/assertions/getting-started.md) |
| Parameterized tests | [Arguments](https://tunit.dev/docs/writing-tests/arguments.md), [method data](https://tunit.dev/docs/writing-tests/method-data-source.md), or [matrix data](https://tunit.dev/docs/writing-tests/matrix-tests.md) |
| Setup, teardown, or hooks | [Lifecycle](https://tunit.dev/docs/writing-tests/lifecycle.md) |
| Fixtures, shared resources, or dependency injection | [Class data sources](https://tunit.dev/docs/writing-tests/class-data-source.md) or [dependency injection](https://tunit.dev/docs/writing-tests/dependency-injection.md) |
| Concurrency or dependencies between tests | [Parallelism](https://tunit.dev/docs/execution/parallelism.md) or [ordering](https://tunit.dev/docs/writing-tests/ordering.md) |
| Run tests or select tests | [Running tests](https://tunit.dev/docs/getting-started/running-your-tests.md) or [filters](https://tunit.dev/docs/execution/test-filters.md) |
| Migrate an existing suite | [xUnit](https://tunit.dev/docs/migration/xunit.md), [NUnit](https://tunit.dev/docs/migration/nunit.md), or [MSTest](https://tunit.dev/docs/migration/mstest.md), matching the source framework |
| Mocking | [TUnit.Mocks](https://tunit.dev/docs/writing-tests/mocking.md) |
| Migrate mocks to TUnit.Mocks | [Moq](https://tunit.dev/docs/migration/mocking/moq.md), [NSubstitute](https://tunit.dev/docs/migration/mocking/nsubstitute.md), or [FakeItEasy](https://tunit.dev/docs/migration/mocking/fakeiteasy.md), matching the source library |
| Aspire integration testing | [Aspire](https://tunit.dev/docs/examples/aspire.md) |
| ASP.NET Core integration testing | [ASP.NET Core](https://tunit.dev/docs/examples/aspnet.md) |
| Playwright browser testing | [Playwright](https://tunit.dev/docs/examples/playwright.md) |
| Testcontainers and container fixtures | [ASP.NET Core integration examples](https://tunit.dev/docs/examples/aspnet.md) (see "With Testcontainers") |
| Native AOT, trimming, or reflection mode | [AOT compatibility](https://tunit.dev/docs/writing-tests/aot.md) or [engine modes](https://tunit.dev/docs/execution/engine-modes.md) |
| Custom assertions | [Assertion source generators](https://tunit.dev/docs/assertions/extensibility/source-generator-assertions.md) or [custom assertion implementations](https://tunit.dev/docs/assertions/extensibility/custom-assertions.md) |
| Custom data sources | [Data source generators](https://tunit.dev/docs/extending/data-source-generators.md) |
| Event receivers | [Event subscribing](https://tunit.dev/docs/writing-tests/event-subscribing.md) |
| TestContext and test metadata | [Test context](https://tunit.dev/docs/writing-tests/test-context.md) |
| Diagnostics, output, or tracing | [Logging](https://tunit.dev/docs/extending/logging.md), [test artifacts](https://tunit.dev/docs/writing-tests/artifacts.md), or [OpenTelemetry](https://tunit.dev/docs/examples/opentelemetry.md) |
| Cancellation | [Cancelling a test](https://tunit.dev/docs/execution/cancellation.md) |
| Discovery, build, or execution failures | [Troubleshooting](https://tunit.dev/docs/troubleshooting.md) |

For other topics, such as analyzers or framework-specific integrations, use the index selectively.

## Essential distinctions

- Await TUnit assertions, for example `await Assert.That(actual).IsEqualTo(expected);`.
- Tests run in parallel by default. Account for shared mutable state and resource lifetimes.
- TUnit uses Microsoft.Testing.Platform and `--treenode-filter`. Match command syntax to the project's SDK and runner mode; do not substitute VSTest `--filter` or assume every `dotnet test` invocation needs a `--` separator.
- Source-generated discovery is the default; reflection mode also exists. Check the relevant documentation when changing discovery or Native AOT behavior.
- TUnit.Assertions and TUnit.Mocks also work with other test frameworks. Using them alone does not require changing the project's test runner.
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<PackageVersion Include="Moq.AutoMock" Version="4.0.3" />
<PackageVersion Include="System.CommandLine" Version="2.0.12" />
<PackageVersion Include="System.ClientModel" Version="1.16.0" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.13" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="OpenAI" Version="2.12.0" />
Expand Down
1 change: 1 addition & 0 deletions EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Moq.AutoMock" />
<PackageReference Include="TUnit" />
<PackageReference Include="TUnit.AspNetCore" />
<PackageReference Include="System.Security.Cryptography.Xml" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>

Expand Down
18 changes: 18 additions & 0 deletions EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ public async Task GetListingAsync_DifferentFileExtension_AutoDiscoversFileExtens
await Assert.That(result.FileExtension).IsEqualTo("xml");
}

[Test]
public async Task GetListingAsync_BookContentFilename_ReturnsListing()
{
ListingSourceCodeService service = CreateService();

ListingSourceCodeResponse? result = await service.GetListingAsync(99, 1);
IReadOnlyList<ListingSourceCodeResponse> chapterListings = await service.GetListingsByChapterAsync(99);

using (Assert.Multiple())
{
await Assert.That(result).IsNotNull();
await Assert.That(result.FileExtension).IsEqualTo("cs");
await Assert.That(result.Content).Contains("BookContent filename");
ListingSourceCodeResponse listing = await Assert.That(chapterListings).HasSingleItem();
await Assert.That(listing.ListingNumber).IsEqualTo(1);
}
}

[Test]
public async Task GetListingsByChapterAsync_WithValidChapter_ReturnsAllListings()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// BookContent filename
Console.WriteLine("BookContent filename");
1 change: 1 addition & 0 deletions EssentialCSharp.Web/EssentialCSharp.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<PackageReference Include="NuGet.Protocol">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Security.Cryptography.Pkcs" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
<PackageReference Include="Octokit" />
Expand Down
8 changes: 3 additions & 5 deletions EssentialCSharp.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,11 @@ private static void Main(string[] args)
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);

// OpenTelemetry — two mutually exclusive export paths:
// Production: Azure Monitor (Application Insights) via APPLICATIONINSIGHTS_CONNECTION_STRING
// Local/Aspire: OTLP to Aspire Dashboard via OTEL_EXPORTER_OTLP_ENDPOINT
// Never both simultaneously — that would cause duplicate telemetry in App Insights.
// Prefer Aspire's OTLP endpoint when present; otherwise export to Azure Monitor.
// This keeps local traces in the dashboard even when a developer has an App Insights connection string configured.
string? appInsightsConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
bool useAzureMonitor = !string.IsNullOrWhiteSpace(appInsightsConnectionString);
bool useOtlp = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
bool useAzureMonitor = !useOtlp && !string.IsNullOrWhiteSpace(appInsightsConnectionString);
bool profilerSupportedPlatform = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
bool profilerSkippedUnsupportedPlatform = false;

Expand Down
24 changes: 12 additions & 12 deletions EssentialCSharp.Web/Services/ListingSourceCodeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,21 @@ public void Dispose()
return null;
}

string pattern = $"{chapterNumber:D2}.{listingNumber:D2}.*";
Regex listingFileRegex = ListingFilePattern();
IFileInfo? matchingFile = directoryContents
.Where(f => !f.IsDirectory)
.FirstOrDefault(f => IsMatch(f.Name, pattern));
.Select(f => new { File = f, Match = listingFileRegex.Match(f.Name) })
.Where(x => x.Match.Success
&& int.Parse(x.Match.Groups[1].Value, CultureInfo.InvariantCulture) == chapterNumber
&& int.Parse(x.Match.Groups[2].Value, CultureInfo.InvariantCulture) == listingNumber)
.OrderBy(x => string.Equals(Path.GetExtension(x.File.Name), ".cs", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(x => x.File.Name, StringComparer.Ordinal)
.Select(x => x.File)
.FirstOrDefault();

if (matchingFile == null)
{
LogListingFileNotFound(_Logger, pattern, chapterDirectory);
LogListingFileNotFound(_Logger, $"{chapterNumber:D2}.{listingNumber:D2}", chapterDirectory);
return null;
}

Expand All @@ -82,7 +89,7 @@ public async Task<IReadOnlyList<ListingSourceCodeResponse>> GetListingsByChapter
return Array.Empty<ListingSourceCodeResponse>();
}

// Regex to match files like "01.01.cs" or "23.15.xml"
// Match both legacy "01.01.cs" and BookContent "Listing01.01.Title.cs" names.
Regex listingFileRegex = ListingFilePattern();

var matchedFiles = directoryContents
Expand Down Expand Up @@ -122,14 +129,7 @@ private static async Task<string> ReadFileContentAsync(IFileInfo file)
return await reader.ReadToEndAsync();
}

private static bool IsMatch(string fileName, string pattern)
{
// Convert glob-like pattern to regex (simple version for our use case)
string regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(fileName, regexPattern);
}

[GeneratedRegex(@"^(\d{2})\.(\d{2})\.(\w+)$")]
[GeneratedRegex(@"^(?:Listing)?(\d{2})\.(\d{2})(?:\..+)?\.(\w+)$")]
private static partial Regex ListingFilePattern();

[LoggerMessage(Level = LogLevel.Information, Message = "Using listing source code from: {Path}")]
Expand Down