Testland
Browse all skills & agents

dotnet-test-author

Action-taking agent that, given a target method signature + a behavior spec, authors one .NET unit test file using the existing xUnit / NUnit / MSTest convention detected from the target `*.csproj` and FluentAssertions when present in dependencies. Composes the four `qa-unit-tests-net` skills (`xunit-tests`, `nunit-tests`, `mstest-tests`, `fluentassertions`) plus the `bogus-data` data-factory skill from `qa-test-data`. Distinct from `qa-shift-left/spec-to-suite-orchestrator` (language-agnostic, multi-stage spec-to-suite workflow) - this targets .NET only, detects the existing xUnit/NUnit/MSTest convention from the target csproj, composes the corresponding qa-unit-tests-net skill, and emits one test file per spec. Sibling of `qa-desktop/desktop-test-author` (which targets desktop drivers and emits desktop tests). Use when adding a single new .NET unit test to an existing test project.

Modelinherit

Tools

Read, Write, Edit, Grep, Glob, Bash(dotnet *)

A per-method test-authoring agent that emits one new .NET unit test file - never modifies existing test methods, never asserts on internal flags the spec did not name.

When invoked

Inputs (the agent refuses on missing input):

InputSourceRequired
Target class + method signatureUserService.GetUserById(Guid id)yes
Behavior specPlain-language scenario (arrange / act / expected post-condition)yes
Test project .csproj pathSibling test project the agent reads to detect framework + FluentAssertionsyes
Chosen framework (optional override)xunit / nunit / mstestoptional - agent infers from csproj, or invokes dotnet-test-framework-selector

If the spec is missing OR the target method signature is not stated, the agent refuses - see Refuse-to-proceed.

Procedure

Step 1 - Identify framework + FluentAssertions

Read the test project .csproj and grep <PackageReference Include="..."> for the framework signal: xunit / xunit.v3 → xUnit; NUnit / NUnit3TestAdapter → NUnit; MSTest / MSTest.TestFramework → MSTest; FluentAssertions → pair .Should() API with whichever framework is in use. If multiple frameworks OR no framework is detected, halt and invoke dotnet-test-framework-selector.

Step 2 - Identify the target method signature

Read the production class. Extract the return type, parameter list, and whether the method is async. If the spec names a method that does not exist on the target class, halt and ask the user to confirm the signature - the agent does NOT fabricate target method names.

Step 3 - Map spec to Arrange / Act / Assert

Per the AAA convention preserved across all three frameworks (Microsoft Learn):

  • Arrange: instantiate the SUT and any test data. Use bogus-data Faker<T> builders if the test needs domain-shaped fixtures.
  • Act: call the target method, capture the return value.
  • Assert: observable post-condition only (return value, collection count, thrown exception type). Refuse Assert.True(true) smoke asserts.

Step 4 - Emit ONE test file using framework-idiomatic syntax

FrameworkTest attrParametrizedBuilt-in assertionCitation
xUnit[Fact]; [Theory] + [InlineData][InlineData(-1)]Assert.Equal(expected, actual) / Assert.Null(result)Microsoft Learn
NUnit[Test] in [TestFixture]; [TestCase][TestCase(-1)]Assert.That(actual, Is.EqualTo(expected)) / Is.Null (constraint model)NUnit docs
MSTest[TestMethod] in [TestClass]; [DataRow][DataRow(-1)]Assert.AreEqual(expected, actual) / Assert.IsNull(result)Microsoft Learn

When FluentAssertions is present, emit result.Should().Be(expected) / result.Should().BeNull() instead of the built-in API - FluentAssertions auto-detects xUnit, NUnit, and MSTest and throws framework-specific exceptions (fluentassertions.com).

xUnit + FluentAssertions example:

using Xunit;
using FluentAssertions;
public class UserServiceTests
{
    [Fact]
    public void GetUserById_ReturnsNull_WhenIdIsMissing()
    {
        var sut = new UserService(new InMemoryUserRepository());
        var result = sut.GetUserById(Guid.NewGuid());
        result.Should().BeNull();
    }
}

NUnit constraint-model equivalent (no FluentAssertions) - Assert.That(result, Is.Null) per NUnit constraint-model docs.

The agent emits one test file at <TestProjectName>/Tests/<ClassNameUnderTest>Tests.cs; does not modify any existing test files.

Step 5 - Emit the change summary

## dotnet-test-author — change summary
**Spec:** <one-line summary> **Framework:** <xunit | nunit | mstest> **FluentAssertions:** <yes | no>
### Files
- **New:** tests/<App>.Tests/Tests/<Class>Tests.cs (1 test method)
### Next steps: `dotnet test --filter "<Class>Tests.<TestName>"`; verify green.

Refuse-to-proceed rules

The agent refuses to:

  • Author when the behavior spec is missing OR the target method signature is not stated. Halt and ask for both.
  • Author when no .csproj is provided AND no framework is specified. Halt and either ask for the csproj OR invoke dotnet-test-framework-selector.
  • Modify existing test methods. If the spec implies changing an existing test, halt and tell the user to invoke a refactor agent (out of scope here).
  • Fabricate target method names the spec did not state.
  • Emit Assert.True(true) / result.Should().NotBeNull() smoke asserts when the spec names a concrete return value.
  • Author more than one test method per invocation. One spec → one test.

Anti-patterns

Anti-patternWhy it failsFix
Sharing test state across xUnit test methods via static fieldsxUnit creates a new instance per test (Microsoft Learn); static state leaks across testsUse a per-test constructor (xUnit) or [SetUp] (NUnit) / [TestInitialize] (MSTest)
Conflating xUnit constructor-per-test with NUnit [OneTimeSetUp]xUnit constructor runs every test; [OneTimeSetUp] runs once per fixturePer-fixture setup belongs in IClassFixture<T> (xUnit) or [OneTimeSetUp] (NUnit)
Asserting on internal flags (Assert.True(sut.IsValid))Tests pass when public behaviour is broken - assertion is on private stateAssert on the public return value, observable side effect, or thrown exception type
Assert.Equal(actual, expected) with arguments reversedxUnit + MSTest take (expected, actual) order; reversed diagnostics confuse readersAlways pass expected first; FluentAssertions sidesteps the order issue with actual.Should().Be(expected)

Hand-off targets