Test-Driven Development
Overview
Write a failing test before writing the code that makes it pass. For bug fixes, reproduce the bug with a test before attempting a fix. Tests are proof — "seems right" is not done. A codebase with good tests is an AI agent's superpower; a codebase without tests is a liability.
When to Use
- Implementing any new logic or behavior
- Fixing any bug (the Prove-It Pattern)
- Modifying existing functionality
- Adding edge case handling
- Any change that could break existing behavior
When NOT to use: Pure configuration changes, documentation updates, or static content changes that have no behavioral impact.
Related: For HTTP and UI integration, combine TDD with integration-testing-dotnet — that skill covers WebApplicationFactory<T>, Testcontainers, Microsoft.Playwright, and Avalonia.Headless for Blazor / ASP.NET Core / Avalonia tests respectively.
Version Awareness: xUnit v2 vs v3, and Microsoft.Testing.Platform
The guidance below works with both current xUnit major versions and both current runners. Detect which one the project uses (the .csproj package references are the tell) and apply the matching packaging.
| Aspect | xUnit v2 (mature) | xUnit v3 (current) |
|---|---|---|
| Core package | xunit |
xunit.v3 |
| IDE / VSTest runner | xunit.runner.visualstudio |
xunit.v3.runner.visualstudio |
Output of dotnet build on the test project |
DLL loaded by the runner | Executable (can be run directly: ./MyApp.Tests.exe) |
| Native runner | VSTest (dotnet test) |
Microsoft.Testing.Platform (MTP) — faster startup, self-contained; still invokable via dotnet test |
| Minimum target framework | netcoreapp3.1+ / net472 | net8.0 or later |
| Source-level API compatibility | [Fact], [Theory], [InlineData], Assert.*, IClassFixture<T>, ICollectionFixture<T>, [Collection] — all identical between v2 and v3 for normal test code |
same |
Practical upshot for the TDD workflow:
- The RED / GREEN / REFACTOR examples in this skill compile unchanged against either version.
dotnet testworks in both worlds. On v3 the command invokes MTP; on v2 it invokes VSTest. Same user-facing command for agents driving tests from CI or a Husky.Net hook.- On v3,
dotnet run --project tests/MyApp.Testsis a legitimate alternative — the test project is an executable — useful when the VSTest runner setup is causing trouble or when you want zero-overhead test startup locally. - MTP filtering syntax differs slightly from VSTest.
dotnet test --filter "FullyQualifiedName~X"still works on both; for v3-native MTP flags see the xUnit v3 docs (link in "See Also"). - xUnit v3 is the current recommendation for new projects on .NET 8+. Existing v2 projects can stay as-is — the test-author API is source-compatible — and migrate when the project has capacity. Samples below are shown in v3 form; swap the package references above to target v2.
MSTest parallel: MSTest has shipped MTP-native support since 3.6. The MSTest.TestFramework + MSTest.Sdk packages give you an MTP-first experience with the same [TestClass]/[TestMethod]/Assert.* API.
The TDD Cycle
RED GREEN REFACTOR
Write a test Write minimal code Clean up the
that fails ──→ to make it pass ──→ implementation ──→ (repeat)
│ │ │
▼ ▼ ▼
Test FAILS Test PASSES Tests still PASS
Step 1: RED — Write a Failing Test
Write the test first. It must fail. A test that passes immediately proves nothing.
Both xUnit and MSTest are first-class in the .NET world; pick one per project and stick to it. Examples below are shown in both using native assertions — Xunit.Assert.X for xUnit, Microsoft.VisualStudio.TestTools.UnitTesting.Assert.X for MSTest. No third-party assertion library is introduced or recommended.
// xUnit v3
public sealed class TaskServiceTests
{
private readonly TaskService _service = new();
[Fact]
public async Task CreateTaskAsync_WithTitle_ReturnsTaskWithDefaultStatus()
{
// RED: This test fails because CreateTaskAsync doesn't exist yet
var task = await _service.CreateTaskAsync(new CreateTaskInput("Buy groceries"));
Assert.NotEqual(default, task.Id);
Assert.Equal("Buy groceries", task.Title);
Assert.Equal(TaskStatus.Pending, task.Status);
Assert.True(task.CreatedAt <= DateTimeOffset.UtcNow);
}
}
// MSTest
[TestClass]
public sealed class TaskServiceTests
{
private readonly TaskService _service = new();
[TestMethod]
public async Task CreateTaskAsync_WithTitle_ReturnsTaskWithDefaultStatus()
{
// RED
var task = await _service.CreateTaskAsync(new CreateTaskInput("Buy groceries"));
Assert.AreNotEqual(default, task.Id);
Assert.AreEqual("Buy groceries", task.Title);
Assert.AreEqual(TaskStatus.Pending, task.Status);
Assert.IsTrue(task.CreatedAt <= DateTimeOffset.UtcNow);
}
}
Step 2: GREEN — Make It Pass
Write the minimum code to make the test pass. Don't over-engineer:
public sealed class TaskService
{
public Task<TaskDto> CreateTaskAsync(CreateTaskInput input, CancellationToken cancellationToken = default)
{
var task = new TaskDto(
Id: TaskId.New(),
Title: input.Title,
Status: TaskStatus.Pending,
CreatedAt: DateTimeOffset.UtcNow);
// Persist via DbContext in a later slice; minimal is "it compiles and passes the test."
return Task.FromResult(task);
}
}
public readonly record struct TaskId(Guid Value)
{
public static TaskId New() => new(Guid.NewGuid());
}
public sealed record CreateTaskInput(string Title);
public sealed record TaskDto(TaskId Id, string Title, TaskStatus Status, DateTimeOffset CreatedAt);
public enum TaskStatus { Pending, InProgress, Completed, Cancelled }
Step 3: REFACTOR — Clean Up
With tests green, improve the code without changing behavior:
- Extract shared logic into private methods or helpers
- Improve naming
- Remove duplication
- Swap
DateTimeOffset.UtcNowfor an injectedTimeProviderso time-dependent tests aren't flaky - Optimize if
BenchmarkDotNetmeasurements justify it
Run dotnet test after every refactor step to confirm nothing broke.
The Prove-It Pattern (Bug Fixes)
When a bug is reported, do not start by trying to fix it. Start by writing a test that reproduces it.
Bug report arrives
│
▼
Write a test that demonstrates the bug
│
▼
Test FAILS (confirming the bug exists)
│
▼
Implement the fix
│
▼
Test PASSES (proving the fix works)
│
▼
Run full test suite (no regressions): `dotnet test`
Example:
// Bug: "Completing a task doesn't update the CompletedAt timestamp"
// Step 1: Write the reproduction test (it should FAIL)
[Fact]
public async Task CompleteTaskAsync_SetsCompletedAtTimestamp()
{
var created = await _service.CreateTaskAsync(new CreateTaskInput("Test"));
var completed = await _service.CompleteTaskAsync(created.Id);
Assert.Equal(TaskStatus.Completed, completed.Status);
Assert.NotNull(completed.CompletedAt); // This fails → bug confirmed
}
// Step 2: Fix the bug
public async Task<TaskDto> CompleteTaskAsync(TaskId id, CancellationToken cancellationToken = default)
{
var task = await _repository.GetByIdAsync(id, cancellationToken);
var completed = task with
{
Status = TaskStatus.Completed,
CompletedAt = _timeProvider.GetUtcNow(), // This was missing
};
await _repository.UpdateAsync(completed, cancellationToken);
return completed;
}
// Step 3: Test passes → bug fixed, regression guarded
The Test Pyramid
Invest testing effort according to the pyramid — most tests should be small and fast, with progressively fewer tests at higher levels:
╱╲
╱ ╲ E2E Tests (~5%)
╱ ╲ Full user flows:
╱──────╲ Blazor/Razor → Microsoft.Playwright
╱ ╲ Avalonia → Avalonia.Headless.XUnit
╱ ╲ Integration Tests (~15%)
╱ ╲ API boundary → WebApplicationFactory<TEntryPoint>
╱──────────────╲ DB → Testcontainers (Postgres/MSSQL/Redis)
╱ ╲ Unit Tests (~80%)
╱ ╲ Pure logic, isolated, milliseconds each
╱────────────────────╲ xUnit v3 / MSTest + native Assert
The Beyonce Rule: If you liked it, you should have put a test on it. Infrastructure changes, refactoring, and migrations are not responsible for catching your bugs — your tests are. If a change breaks your code and you didn't have a test for it, that's on you.
Test Sizes (Resource Model)
Beyond the pyramid levels, classify tests by what resources they consume:
| Size | Constraints | Speed | Example in .NET |
|---|---|---|---|
| Small | Single process, no I/O, no network, no database | Milliseconds | Pure domain tests in MyApp.Core.Tests, value-type tests |
| Medium | Multi-process OK, localhost only, no external services | Seconds | WebApplicationFactory HTTP tests with SQLite or Testcontainers; Avalonia.Headless UI tests |
| Large | Multi-machine OK, external services allowed | Minutes | Full Microsoft.Playwright E2E runs against a staged deployment; BenchmarkDotNet perf suites |
Small tests should make up the vast majority of your suite. They're fast, reliable, and easy to debug when they fail.
Decision Guide
Is it pure logic with no side effects?
→ Unit test (xUnit/MSTest in MyApp.Core.Tests)
Does it cross a boundary (HTTP, EF Core, file system, message bus)?
→ Integration test (WebApplicationFactory + Testcontainers in MyApp.Integration.Tests)
Is it a critical user flow that must work end-to-end in a real browser or UI?
→ E2E test (Playwright.NET or Avalonia.Headless) — limit these to critical paths
Writing Good Tests
Test State, Not Interactions
Assert on the outcome of an operation, not on which methods were called internally. Tests that verify method-call sequences break when you refactor, even if the behavior is unchanged.
// Good: Tests what the service does (state-based)
[Fact]
public async Task ListTasks_SortedByCreationDate_NewestFirst()
{
var tasks = await _service.ListAsync(new ListTasksParams(SortBy: "createdAt", SortOrder: "desc"));
Assert.Equal(tasks.Data.OrderByDescending(t => t.CreatedAt), tasks.Data);
}
// Bad: Tests how the service works internally (interaction-based using Moq/NSubstitute)
[Fact]
public async Task ListTasks_CallsDbContextWithOrderByDesc()
{
await _service.ListAsync(new ListTasksParams(SortBy: "createdAt", SortOrder: "desc"));
_dbContextMock.Verify(db => db.Tasks.OrderByDescending(It.IsAny<Expression<Func<Task, DateTimeOffset>>>()),
Times.Once);
}
DAMP Over DRY in Tests
In production code, DRY (Don't Repeat Yourself) is usually right. In tests, DAMP (Descriptive And Meaningful Phrases) is better. A test should read like a specification — each test should tell a complete story without requiring the reader to trace through shared helpers.
// DAMP: Each test is self-contained and readable
[Fact]
public void CreateTask_WithEmptyTitle_Throws()
{
var input = new CreateTaskInput("");
var ex = Assert.Throws<ValidationException>(() => _service.Create(input));
Assert.Contains("Title is required", ex.Message);
}
[Fact]
public void CreateTask_TrimsWhitespaceFromTitle()
{
var input = new CreateTaskInput(" Buy groceries ");
var task = _service.Create(input);
Assert.Equal("Buy groceries", task.Title);
}
// Over-DRY: Shared parameterized setup that obscures what each test actually verifies
// (Don't do this just to avoid repeating the input shape)
Some duplication in tests is acceptable when it makes each test independently understandable. Reach for [Theory] / [InlineData] (xUnit) or [DataRow] (MSTest) when the variation itself is the point of the test, not to collapse conceptually different tests into one.
Prefer Real Implementations Over Mocks
Use the simplest test double that gets the job done. The more your tests use real code, the more confidence they provide.
Preference order (most to least preferred):
1. Real implementation → Highest confidence, catches real bugs
2. Fake → In-memory version (e.g., EF Core InMemory provider, in-memory IDistributedCache)
3. Stub → Returns canned data, no behavior (hand-written Test class that implements the interface)
4. Mock (interaction) → Moq / NSubstitute verifying method calls — use sparingly
For EF Core, prefer SQLite in-memory or Testcontainers over the Microsoft.EntityFrameworkCore.InMemory provider — the in-memory provider doesn't enforce relational constraints, so tests pass against it that would fail in production.
Use mocks only when: the real implementation is too slow (external APIs), non-deterministic (random, wall-clock time), or has side effects you can't control (email sending, payment processing). Over-mocking creates tests that pass while production breaks.
Use TimeProvider for Time-Dependent Logic
.NET 8+ ships TimeProvider — inject it instead of calling DateTimeOffset.UtcNow directly, then use FakeTimeProvider (from Microsoft.Extensions.TimeProvider.Testing) in tests:
public sealed class TaskService(ITaskRepository repository, TimeProvider timeProvider)
{
public TaskDto MarkOverdue(TaskDto task)
{
var now = timeProvider.GetUtcNow();
return task with { IsOverdue = task.Deadline < now };
}
}
[Fact]
public void MarkOverdue_DeadlinePassed_SetsOverdueFlag()
{
var time = new FakeTimeProvider(new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero));
var service = new TaskService(_repository, time);
var task = new TaskDto(/* ... */, Deadline: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
var result = service.MarkOverdue(task);
Assert.True(result.IsOverdue);
}
Use the Arrange-Act-Assert Pattern
[Fact]
public void MarkOverdue_DeadlinePassed_SetsOverdueFlag()
{
// Arrange
var time = new FakeTimeProvider(new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero));
var service = new TaskService(_repository, time);
var task = new TaskDto(/* ... */, Deadline: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
// Act
var result = service.MarkOverdue(task);
// Assert
Assert.True(result.IsOverdue);
}
One Assertion Per Concept
// Good: Each test verifies one behavior
[Fact] public void Create_RejectsEmptyTitle() { /* ... */ }
[Fact] public void Create_TrimsWhitespaceFromTitle() { /* ... */ }
[Fact] public void Create_EnforcesMaxTitleLength() { /* ... */ }
// Bad: Everything in one test
[Fact]
public void Create_ValidatesTitlesCorrectly()
{
Assert.Throws<ValidationException>(() => _service.Create(new("")));
Assert.Equal("hello", _service.Create(new(" hello ")).Title);
Assert.Throws<ValidationException>(() => _service.Create(new(new string('a', 256))));
}
Name Tests Descriptively
A common .NET convention is MethodUnderTest_Scenario_ExpectedResult:
// Good: reads like a specification
public class TaskService_CompleteTaskAsync_Tests
{
[Fact] public Task SetsStatusToCompletedAndRecordsTimestamp() { /* ... */ return Task.CompletedTask; }
[Fact] public Task ThrowsNotFoundException_WhenTaskDoesNotExist() { /* ... */ return Task.CompletedTask; }
[Fact] public Task IsIdempotent_CompletingAlreadyCompletedTaskIsNoOp() { /* ... */ return Task.CompletedTask; }
[Fact] public Task SendsNotificationToAssignee() { /* ... */ return Task.CompletedTask; }
}
// Bad: Vague names
public class TaskServiceTests
{
[Fact] public void Works() { }
[Fact] public void HandlesErrors() { }
[Fact] public void Test3() { }
}
Test Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Testing implementation details | Tests break when refactoring even if behavior is unchanged | Test inputs and outputs, not internal structure |
| Flaky tests (timing, order-dependent) | Erode trust in the test suite; [Fact(Skip = "…")] accumulates |
Use deterministic assertions, FakeTimeProvider, per-test DbContext scoping |
| Testing framework code | Wastes time testing BCL/EF Core behavior | Only test YOUR code |
| Snapshot abuse (e.g. Verify.Xunit on sprawling objects) | Large snapshots nobody reviews, break on any change | Use sparingly and review every change |
| No test isolation | Tests pass individually but fail together (shared static state, shared DbContext) |
Each test sets up and tears down its own fixture; use IClassFixture / ICollectionFixture intentionally |
| Mocking everything | Tests pass but production breaks | Prefer real > fakes > stubs > mocks. Mock only at non-deterministic or external boundaries |
Microsoft.EntityFrameworkCore.InMemory for anything serious |
Doesn't enforce relational constraints; false confidence | Use SQLite in-memory or Testcontainers with the real provider |
.Result / .Wait() in test bodies |
Deadlocks; muddies stack traces | async Task test methods and await everywhere |
When to Use Subagents for Testing
For complex bug fixes, spawn a subagent to write the reproduction test:
Main agent: "Spawn a subagent to write a test that reproduces this bug:
[bug description]. The test should fail with the current code."
Subagent: Writes the reproduction test (xUnit or MSTest, matching
the project convention).
Main agent: Verifies the test fails (`dotnet test --filter ...`),
then implements the fix, then verifies the test passes.
This separation ensures the test is written without knowledge of the fix, making it more robust.
See Also
- Upstream testing-patterns reference (generic, pre-dates this adaptation): https://github.com/addyosmani/agent-skills/blob/44dac80216da709913fb410f632a65547866346f/references/testing-patterns.md
- For HTTP / browser / desktop integration testing, see
integration-testing-dotnet— coversWebApplicationFactory<TEntryPoint>, Testcontainers,Microsoft.Playwright, andAvalonia.Headless.XUnit - xUnit: https://xunit.net/ (covers both v2 and v3; v3's "Getting Started" section describes the MTP-native workflow)
- MSTest: https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-intro
- Microsoft.Testing.Platform: https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro
TimeProvider: https://learn.microsoft.com/dotnet/api/system.timeprovider
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll write tests after the code works" | You won't. And tests written after the fact test implementation, not behavior. |
| "This is too simple to test" | Simple code gets complicated. The test documents the expected behavior. |
| "Tests slow me down" | Tests slow you down now. They speed you up every time you change the code later. |
| "I tested it manually" | Manual testing doesn't persist. Tomorrow's change might break it with no way to know. |
| "The code is self-explanatory" | Tests ARE the specification. They document what the code should do, not what it does. |
| "It's just a prototype" | Prototypes become production code. Tests from day one prevent the "test debt" crisis. |
| "EF Core InMemory is good enough for tests" | It skips relational constraints. You'll discover that the day you deploy to Postgres. Use SQLite in-memory or Testcontainers. |
| "I'll use DateTime.UtcNow — it's fine" | Time-dependent tests go flaky the minute someone runs them on a slow CI machine. Inject TimeProvider from day one. |
"Let me run dotnet test again just to be extra sure" |
After a clean test run, repeating the same command adds nothing unless the code has changed since. Run again after subsequent edits, not as reassurance. |
Red Flags
- Writing code without any corresponding tests
- Tests that pass on the first run (they may not be testing what you think)
- "All tests pass" but
dotnet testoutput showsSkippedorPassed: 0 - Bug fixes without reproduction tests
- Tests that test BCL / EF Core behavior instead of application behavior
- Test names that don't describe the expected behavior
[Fact(Skip = "…")]/[Ignore]added to make the suite pass.Resultor.Wait()in test bodies- Mocking
DbContextdirectly instead of using a real provider (SQLite or Testcontainers) - A solution with no
tests/directory - Running the same
dotnet testcommand twice in a row without any intervening code change
Verification
After completing any implementation:
- Every new behavior has a corresponding xUnit or MSTest test
- All tests pass:
dotnet test - Bug fixes include a reproduction test that failed before the fix
- Test names describe the behavior being verified (
MethodUnderTest_Scenario_Expectedconvention) - No tests are
[Fact(Skip = "…")]/[Ignore]without a linked issue - Time-dependent logic uses
TimeProviderinjected via DI; tests useFakeTimeProvider - Integration tests use a real provider (SQLite or Testcontainers), not
EntityFrameworkCore.InMemory - Coverage hasn't decreased (
dotnet test --collect:"XPlat Code Coverage"if tracked)
Note: Run each test command after a change that could affect the result. After a clean dotnet test run, don't repeat the same command unless the code has changed since — re-running on unchanged code adds no confidence.
Source & Modifications
- Upstream: https://github.com/addyosmani/agent-skills/blob/44dac80216da709913fb410f632a65547866346f/skills/test-driven-development/SKILL.md
- Pinned commit:
44dac80216da709913fb410f632a65547866346f(synced 2026-04-19) - Status:
modified - Changes:
- All TypeScript/Jest examples rewritten as dual xUnit + MSTest (with FluentAssertions note) for both the RED step and the Prove-It Pattern; GREEN step uses C# records +
TaskIdstrongly-typed ID +TimeProvider - REFACTOR step mentions
TimeProviderinjection andBenchmarkDotNet-justified optimization - Test Pyramid labels call out the concrete .NET tooling at each tier (xUnit/MSTest at the base,
WebApplicationFactory+ Testcontainers at the middle, Playwright.NET + Avalonia.Headless at the top) - Test Sizes table retargeted:
MyApp.Core.Tests,WebApplicationFactory, SQLite/Testcontainers, Playwright E2E, BenchmarkDotNet - Decision Guide uses .NET-specific project names (
MyApp.Integration.Tests, Playwright.NET, Avalonia.Headless) - State-vs-interaction example rewritten with xUnit + FluentAssertions and Moq counter-example
- DAMP example rewritten in C# with
[Theory]/[InlineData](xUnit) and[DataRow](MSTest) pointer for parameterized variation - "Prefer Real Implementations" table mentions EF Core SQLite in-memory, Testcontainers, Moq/NSubstitute; added explicit warning against
Microsoft.EntityFrameworkCore.InMemory(no relational constraint enforcement) - Added new "Use
TimeProviderfor Time-Dependent Logic" section (.NET 8+TimeProvider+FakeTimeProviderfromMicrosoft.Extensions.TimeProvider.Testing) — not in upstream - Arrange-Act-Assert + One-Assertion-Per-Concept + Name-Tests-Descriptively examples rewritten with xUnit +
MethodUnderTest_Scenario_Expectednaming convention - Anti-patterns table adds:
EntityFrameworkCore.InMemoryfalse confidence,.Result/.Wait()deadlocks,IClassFixture/ICollectionFixturescoping,Verify.Xunitsnapshot abuse - Removed upstream's "Browser Testing with DevTools" section entirely and replaced with a pointer to the new
integration-testing-dotnetskill (which covers the .NET equivalent) - Subagent section uses
dotnet test --filter - Added "See Also" links to xUnit docs, MSTest docs,
TimeProviderAPI reference - Rationalizations table adds rows on EF Core InMemory and
DateTime.UtcNowvsTimeProvider - Red-flag list adds
.Result/.Wait(), mockingDbContext, missingtests/directory,[Fact(Skip = "…")] - Verification checklist retargeted to
dotnet test,TimeProvider+FakeTimeProvider, real EF Core provider,--collect:"XPlat Code Coverage" - Preserved verbatim: TDD cycle diagram, Step 1/2/3 structure, Prove-It Pattern flowchart, Test Pyramid diagram, Beyonce Rule, Arrange-Act-Assert frame, Common Rationalizations and Red Flags structure
- All TypeScript/Jest examples rewritten as dual xUnit + MSTest (with FluentAssertions note) for both the RED step and the Prove-It Pattern; GREEN step uses C# records +
- Downstream patches (applied after the initial sync; not tracked against upstream):
- 2026-04-19 (skill v1.0.1) — Added "Version Awareness: xUnit v2 vs v3, and Microsoft.Testing.Platform" section covering package-reference differences (
xunitvsxunit.v3), test-project-as-executable output in v3, VSTest vs MTP runner differences,dotnet run --project tests/…as a v3-only alternative todotnet test, source-level API compatibility for[Fact]/Assert.*/IClassFixture<T>, MSTest's parallel MTP-native story viaMSTest.Sdk. Added MTP docs to "See Also". Description updated to mention "(v2 or v3)" and "VSTest and Microsoft.Testing.Platform (MTP) runners". - 2026-04-19 (skill v1.0.2, plugin v2.3.0) — FluentAssertions removed and xUnit v3 made canonical. Rationale: FluentAssertions v8 (January 2025) moved to the XCEED source-available license; only v7.x remains Apache 2.0, and recommending "FluentAssertions" without pinning nudges agents toward v8. Every sample now uses native
Xunit.Assert.Xor MSTestAssert.X. The optional "FluentAssertions alternative" block after the RED step is deleted. Prove-It, State-vs-Interaction, DAMP, TimeProvider, and Arrange-Act-Assert samples rewritten —completed.CompletedAt.Should().NotBeNull()→Assert.NotNull(completed.CompletedAt),tasks.Data.Should().BeInDescendingOrder(...)→Assert.Equal(tasks.Data.OrderByDescending(...), tasks.Data),act.Should().Throw<E>().WithMessage("*X*")→var ex = Assert.Throws<E>(act); Assert.Contains("X", ex.Message),result.IsOverdue.Should().BeTrue()→Assert.True(result.IsOverdue). Test-pyramid diagram label changedxUnit / MSTest + FluentAssertions→xUnit v3 / MSTest + native Assert. Version Awareness "practical upshot" reframed — v3 is the current recommendation for new projects; v2 stays source-compatible for test authors. - Upstream sync 2026-06-14 (plugin v2.6.0) — re-pinned to
3a6fc63. Ported the upstream "no redundant re-runs" guidance (a new rationalization row, a new red-flag, and a**Note:**after the Verification checklist): after a cleandotnet testrun, re-running the same command on unchanged code adds no confidence. Retargeted from upstream's generic test wording to thedotnetCLI.
- 2026-04-19 (skill v1.0.1) — Added "Version Awareness: xUnit v2 vs v3, and Microsoft.Testing.Platform" section covering package-reference differences (
- License: MIT © 2025 Addy Osmani — see
../../LICENSES/agent-skills-MIT.txt