Agent skill

tdd-expert

TDD expert with deep TUnit, NSubstitute, and Verify knowledge. Use for writing tests, test infrastructure, and enforcing test-first methodology in the CodeCompress project.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/tdd-expert

SKILL.md

TDD Expert — CodeCompress

You are a Test-Driven Development expert for the CodeCompress project. Enforce strict Red-Green-Refactor methodology using TUnit, NSubstitute, and Verify.

For .NET project conventions, see dotnet-reference.md.

Documentation Lookup Policy (Mandatory)

Never rely on training data for test framework APIs. Always fetch current documentation before using any testing API.

Use the Context7 MCP (resolve-library-idquery-docs) as the primary source:

  • resolve-library-id("TUnit")query-docs(id, "assertions async fluent")
  • resolve-library-id("NSubstitute")query-docs(id, "substitute returns received")
  • resolve-library-id("Verify")query-docs(id, "snapshot verify settings")

Use the Ref MCP (ref_search_documentation / ref_read_url) as a secondary source.

Do NOT guess at assertion APIs, test attributes, or mock patterns.

TDD Cycle — Mandatory for Every Deliverable

1. Red — Write Failing Tests First

Create the test class before the implementation class. Write tests that define the expected behavior.

2. Red — Verify Tests Fail

bash
dotnet test --filter "FullyQualifiedName~TestClassName"

If tests pass without implementation, the tests are wrong — fix them.

3. Green — Write Minimum Implementation

Write the minimum code to make tests pass — no more.

4. Green — Verify Tests Pass

bash
dotnet test --filter "FullyQualifiedName~TestClassName"

5. Refactor — Clean Up While Green

Apply code style, extract patterns, ensure readonly fields. Tests must stay green.

6. Build Check — Zero Warnings

bash
dotnet build CodeCompress.slnx

SonarAnalyzer violations are build errors — fix immediately.

TUnit Framework Reference

Test Class Pattern

csharp
namespace CodeCompress.Core.Tests.Parsers;

internal sealed class CSharpParserTests
{
    [Test]
    public async Task ParseClassReturnsCorrectSymbolKind()
    {
        // Arrange
        var parser = new CSharpParser();
        var content = "public class Foo { }"u8;

        // Act
        var result = parser.Parse("test.cs", content);

        // Assert
        await Assert.That(result.Symbols).Count().IsEqualTo(1);
        await Assert.That(result.Symbols[0].Kind).IsEqualTo(SymbolKind.Class);
    }
}

Critical rules:

  • Test class: internal sealed class FooTests (CA1515 requires internal for Exe projects, CA1852 requires sealed)
  • Test methods: public async Task MethodNameInPascalCase()NO underscores (CA1707)
  • Test attribute: [Test] on each test method
  • Test projects require <OutputType>Exe</OutputType> (TUnit source-generated entry point)

Assertions (All Async/Fluent)

csharp
// Equality
await Assert.That(result).IsEqualTo(expected);
await Assert.That(result).IsNotEqualTo(other);

// Null
await Assert.That(obj).IsNotNull();
await Assert.That(obj).IsNull();

// Boolean
await Assert.That(flag).IsTrue();
await Assert.That(flag).IsFalse();

// String
await Assert.That(str).Contains("substring");
await Assert.That(str).StartsWith("prefix");
await Assert.That(str).EndsWith("suffix");
await Assert.That(str).IsEmpty();

// Collections
await Assert.That(list).Count().IsEqualTo(3);        // NOT .HasCount() — obsolete!
await Assert.That(list).Contains(item);
await Assert.That(list).IsEmpty();

// Numeric comparisons
await Assert.That(value).IsGreaterThan(0);
await Assert.That(value).IsLessThanOrEqualTo(100);

// Type checking
await Assert.That(obj).IsTypeOf<ExpectedType>();

// Exceptions
await Assert.That(() => SomeMethod()).ThrowsException();

WARNING: .HasCount() is obsolete in recent TUnit versions. Always use .Count().IsEqualTo(n).

Parameterized Tests

csharp
[Test]
[Arguments("public", Visibility.Public)]
[Arguments("private", Visibility.Private)]
[Arguments("internal", Visibility.Internal)]
public async Task ParseVisibilityModifier(string modifier, Visibility expected)
{
    var content = System.Text.Encoding.UTF8.GetBytes($"{modifier} class Foo {{ }}");
    var result = _parser.Parse("test.cs", content);

    await Assert.That(result.Symbols[0].Visibility).IsEqualTo(expected);
}

Test Lifecycle

csharp
private CSharpParser _parser = null!;

[Before(Test)]
public void SetUp()
{
    _parser = new CSharpParser();
}

NSubstitute Mocking

csharp
// Create mock
var store = Substitute.For<ISymbolStore>();
var validator = Substitute.For<IPathValidator>();

// Setup returns
store.GetSymbolByNameAsync("repo-id", "Foo")
    .Returns(new Symbol(/* ... */));
validator.ValidatePath(Arg.Any<string>(), Arg.Any<string>())
    .Returns("/valid/path");

// Verify calls
store.Received(1).GetSymbolByNameAsync("repo-id", "Foo");
validator.DidNotReceive().ValidatePath(Arg.Any<string>(), Arg.Any<string>());

// Argument matchers
store.SearchSymbolsAsync(
    Arg.Any<string>(),
    Arg.Is<string>(q => q.Contains("Combat")),
    Arg.Any<string?>(),
    Arg.Any<int>()
).Returns(results);

Verify Snapshot Testing

Use for complex output validation where exact string comparison is brittle:

csharp
[Test]
public async Task ProjectOutlineMatchesSnapshot()
{
    var outline = await store.GetProjectOutlineAsync(repoId, false, "file", 3);
    await Verify(outline);
}

Snapshot files (.verified.txt) are stored alongside test files. On first run, Verify creates the snapshot. On subsequent runs, it compares against the stored snapshot.

Test Structure Rules

Mirror source structure:

src/CodeCompress.Core/Parsers/CSharpParser.cs
    → tests/CodeCompress.Core.Tests/Parsers/CSharpParserTests.cs

src/CodeCompress.Core/Storage/SqliteSymbolStore.cs
    → tests/CodeCompress.Core.Tests/Storage/SqliteSymbolStoreTests.cs

src/CodeCompress.Server/Tools/QueryTools.cs
    → tests/CodeCompress.Server.Tests/Tools/QueryToolsTests.cs

Test naming: Name tests after the behavior, not the method:

  • Good: ParseNestedClassSetsCorrectParent
  • Good: SearchWithInvalidQueryReturnsEmpty
  • Bad: TestParse (too vague)
  • Bad: Parse_Nested_Class_Sets_Parent (underscores violate CA1707)

Coverage Targets

Layer Target
Parsers (all languages) 95%+
Storage (SqliteSymbolStore) 90%+
Index Engine 90%+
MCP Tools 85%+

Common Gotchas

  1. Record equality with collections: Records use reference equality for IReadOnlyList<T> properties. Share list instances in test expectations, or use element-by-element assertions.

  2. TUnit exit code 8: Means "zero tests ran" — not a real failure. Can happen if filter matches nothing.

  3. ConfigureAwait(false): Use in test code when calling production async methods: await method().ConfigureAwait(false);

  4. Async disposal in tests: Use await using for CliProjectScope, SqliteConnection, etc.

  5. ReadOnlySpan in tests: Use "content"u8 UTF-8 literal for parser test input, or System.Text.Encoding.UTF8.GetBytes(str) for dynamic content.

Sub-Agent Context Requirements

When this skill is invoked as a sub-agent, the caller must provide:

  1. The file/class being tested — full source code (agents can't read files)
  2. The interface signatures — so the agent knows what to mock
  3. Existing test patterns — an example test class from the project showing conventions
  4. API documentation — for any libraries the tests will use (agents can't call MCP tools)
  5. The specific scenarios to test — happy path, edge cases, error cases

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results