Claude Skill

dotnet-nunit

Write, run, or repair .NET tests that use NUnit. Use when a repo uses `NUnit`, `[Test]`, `[TestCase]`, `[TestFixture]`, or NUnit3TestAdapter for VSTest or Microsoft.Testing.Platform execution.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-nunit-bfa4ebd.zip · 8 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

Install

skills CLI npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-nunit
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

NUnit Testing

Trigger On

  • writing or reviewing NUnit tests
  • using [Test], [TestCase], [TestFixture], [SetUp], [TearDown] attributes
  • configuring NUnit3TestAdapter or NUnit.Analyzers
  • migrating between NUnit versions
  • integrating NUnit with CI pipelines

Documentation

References

Package Selection

Package Purpose
NUnit Core testing framework
NUnit3TestAdapter VSTest adapter for dotnet test
NUnit.Analyzers Roslyn analyzers for NUnit best practices
Microsoft.NET.Test.Sdk Required for test discovery

Project Setup

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
    <PackageReference Include="NUnit" Version="4.*" />
    <PackageReference Include="NUnit3TestAdapter" Version="4.*" />
    <PackageReference Include="NUnit.Analyzers" Version="4.*">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>

Test Patterns

Basic Test Structure

[TestFixture]
public class CalculatorTests
{
    private Calculator _calculator;

    [SetUp]
    public void SetUp()
    {
        _calculator = new Calculator();
    }

    [TearDown]
    public void TearDown()
    {
        _calculator?.Dispose();
    }

    [Test]
    public void Add_TwoPositiveNumbers_ReturnsSum()
    {
        var result = _calculator.Add(2, 3);

        Assert.That(result, Is.EqualTo(5));
    }

    [Test]
    public void Divide_ByZero_ThrowsException()
    {
        Assert.Throws<DivideByZeroException>(() => _calculator.Divide(10, 0));
    }
}

Parameterized Tests with TestCase

[TestFixture]
public class ValidationTests
{
    [TestCase("", false)]
    [TestCase("a", false)]
    [TestCase("ab", false)]
    [TestCase("abc", true)]
    [TestCase("valid@email.com", true)]
    public void IsValid_VariousInputs_ReturnsExpected(string input, bool expected)
    {
        var result = Validator.IsValid(input);

        Assert.That(result, Is.EqualTo(expected));
    }

    [TestCase(1, 2, ExpectedResult = 3)]
    [TestCase(-1, 1, ExpectedResult = 0)]
    [TestCase(100, 200, ExpectedResult = 300)]
    public int Add_TestCases_ReturnsExpectedResult(int a, int b)
    {
        return _calculator.Add(a, b);
    }
}

TestCaseSource for Complex Data

[TestFixture]
public class OrderTests
{
    private static IEnumerable<TestCaseData> OrderTestCases()
    {
        yield return new TestCaseData(
            new Order { Items = new[] { new Item { Price = 10 }, new Item { Price = 20 } } },
            30m
        ).SetName("TwoItems_CalculatesTotal");

        yield return new TestCaseData(
            new Order { Items = Array.Empty<Item>() },
            0m
        ).SetName("EmptyOrder_ReturnsZero");

        yield return new TestCaseData(
            new Order { Items = new[] { new Item { Price = 100 } }, DiscountPercent = 10 },
            90m
        ).SetName("WithDiscount_AppliesDiscount");
    }

    [TestCaseSource(nameof(OrderTestCases))]
    public void CalculateTotal_VariousOrders_ReturnsExpected(Order order, decimal expected)
    {
        var result = order.CalculateTotal();

        Assert.That(result, Is.EqualTo(expected));
    }
}

Constraint-Based Assertions

[Test]
public void AssertionExamples()
{
    // Equality
    Assert.That(actual, Is.EqualTo(expected));
    Assert.That(actual, Is.Not.EqualTo(other));

    // Comparison
    Assert.That(value, Is.GreaterThan(5));
    Assert.That(value, Is.LessThanOrEqualTo(10));
    Assert.That(value, Is.InRange(1, 100));

    // String
    Assert.That(str, Does.StartWith("Hello"));
    Assert.That(str, Does.Contain("world"));
    Assert.That(str, Does.Match(@"\d{3}-\d{4}"));
    Assert.That(str, Is.EqualTo("HELLO").IgnoreCase);

    // Collection
    Assert.That(list, Has.Count.EqualTo(5));
    Assert.That(list, Contains.Item("expected"));
    Assert.That(list, Is.All.GreaterThan(0));
    Assert.That(list, Is.Unique);
    Assert.That(list, Is.Ordered);
    Assert.That(list, Has.Exactly(3).Items.GreaterThan(10));

    // Type
    Assert.That(obj, Is.InstanceOf<MyClass>());
    Assert.That(obj, Is.AssignableTo<IMyInterface>());

    // Null
    Assert.That(obj, Is.Null);
    Assert.That(obj, Is.Not.Null);

    // Boolean
    Assert.That(condition, Is.True);
    Assert.That(condition, Is.False);

    // Exception
    Assert.That(() => DoSomething(), Throws.TypeOf<InvalidOperationException>());
    Assert.That(() => DoSomething(), Throws.Exception.With.Message.Contains("error"));

    // Async
    Assert.That(async () => await DoAsync(), Throws.Nothing);
}

Async Test Support

[TestFixture]
public class AsyncServiceTests
{
    private IAsyncService _service;

    [SetUp]
    public void SetUp()
    {
        _service = new AsyncService();
    }

    [Test]
    public async Task GetDataAsync_ValidId_ReturnsData()
    {
        var result = await _service.GetDataAsync(1);

        Assert.That(result, Is.Not.Null);
        Assert.That(result.Id, Is.EqualTo(1));
    }

    [Test]
    public void GetDataAsync_InvalidId_ThrowsException()
    {
        Assert.ThrowsAsync<NotFoundException>(
            async () => await _service.GetDataAsync(-1));
    }

    [Test]
    public async Task ProcessAsync_CancellationRequested_ThrowsOperationCanceled()
    {
        using var cts = new CancellationTokenSource();
        cts.Cancel();

        Assert.ThrowsAsync<OperationCanceledException>(
            async () => await _service.ProcessAsync(cts.Token));
    }
}

OneTimeSetUp and OneTimeTearDown

[TestFixture]
public class IntegrationTests
{
    private static TestServer _server;
    private HttpClient _client;

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // Runs once before all tests in fixture
        _server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
    }

    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        // Runs once after all tests in fixture
        _server?.Dispose();
    }

    [SetUp]
    public void SetUp()
    {
        // Runs before each test
        _client = _server.CreateClient();
    }

    [TearDown]
    public void TearDown()
    {
        // Runs after each test
        _client?.Dispose();
    }

    [Test]
    public async Task GetEndpoint_ReturnsSuccess()
    {
        var response = await _client.GetAsync("/api/data");
        Assert.That(response.IsSuccessStatusCode, Is.True);
    }
}

Categories and Filtering

[TestFixture]
[Category("Integration")]
public class DatabaseTests
{
    [Test]
    [Category("Slow")]
    public void SlowDatabaseTest() { }

    [Test]
    [Category("Fast")]
    public void FastDatabaseTest() { }
}

// Run specific categories:
// dotnet test --filter "Category=Fast"
// dotnet test --filter "Category!=Slow"

Anti-Patterns to Avoid

Anti-Pattern Why It's Bad Better Approach
Multiple asserts without clear purpose Hard to identify which assertion failed One logical assertion per test or use Assert.Multiple
Test interdependence Tests fail unpredictably Each test should be independent
Hardcoded test data Brittle tests Use TestCase or TestCaseSource
Testing implementation details Breaks on refactoring Test behavior, not internals
Missing [SetUp]/[TearDown] cleanup Resource leaks Always clean up resources
Classic Assert syntax Less readable Use constraint model (Assert.That)

Running Tests

# Run all tests
dotnet test

# Run with verbosity
dotnet test --logger "console;verbosity=detailed"

# Filter by name
dotnet test --filter "FullyQualifiedName~CalculatorTests"

# Filter by category
dotnet test --filter "Category=Unit"

# Run in parallel
dotnet test -- NUnit.NumberOfTestWorkers=4

Deliver

  • NUnit tests following the Arrange-Act-Assert pattern
  • Parameterized tests with [TestCase] and [TestCaseSource]
  • Constraint-based assertions with Assert.That
  • Proper test lifecycle management

Validate

  • Tests are independent and isolated
  • No hardcoded test data where parameterization is appropriate
  • Async tests use async Task not async void
  • Resources are properly disposed in [TearDown] or [OneTimeTearDown]
  • NUnit.Analyzers enabled to catch common issues
Files (dotnet-skills)
  • references
    • anti-patterns.md 10.2 KB
      # NUnit Anti-Patterns Reference
      
      ## Test Structure Anti-Patterns
      
      ### Multiple Unrelated Assertions
      
      ```csharp
      // WRONG - Tests multiple unrelated things
      [Test]
      public void UserService_Works()
      {
          var user = _service.CreateUser("test@example.com");
          Assert.That(user, Is.Not.Null);
      
          var updated = _service.UpdateUser(user.Id, "new@example.com");
          Assert.That(updated.Email, Is.EqualTo("new@example.com"));
      
          _service.DeleteUser(user.Id);
          var deleted = _service.GetUser(user.Id);
          Assert.That(deleted, Is.Null);
      }
      
      // CORRECT - One test per behavior
      [Test]
      public void CreateUser_ValidEmail_ReturnsUser()
      {
          var user = _service.CreateUser("test@example.com");
          Assert.That(user, Is.Not.Null);
      }
      
      [Test]
      public void UpdateUser_NewEmail_UpdatesEmail()
      {
          var user = _service.CreateUser("test@example.com");
          var updated = _service.UpdateUser(user.Id, "new@example.com");
          Assert.That(updated.Email, Is.EqualTo("new@example.com"));
      }
      
      [Test]
      public void DeleteUser_ExistingUser_RemovesUser()
      {
          var user = _service.CreateUser("test@example.com");
          _service.DeleteUser(user.Id);
          Assert.That(_service.GetUser(user.Id), Is.Null);
      }
      ```
      
      ### Test Interdependence
      
      ```csharp
      // WRONG - Tests depend on execution order
      [TestFixture]
      public class OrderDependentTests
      {
          private static int _sharedCounter = 0;
      
          [Test, Order(1)]
          public void First_IncrementsCounter()
          {
              _sharedCounter++;
              Assert.That(_sharedCounter, Is.EqualTo(1));
          }
      
          [Test, Order(2)]
          public void Second_DependsOnFirst()
          {
              // Fails if First doesn't run first
              Assert.That(_sharedCounter, Is.EqualTo(1));
          }
      }
      
      // CORRECT - Independent tests
      [TestFixture]
      public class IndependentTests
      {
          private int _counter;
      
          [SetUp]
          public void SetUp()
          {
              _counter = 0; // Fresh state for each test
          }
      
          [Test]
          public void Increment_FromZero_ReturnsOne()
          {
              _counter++;
              Assert.That(_counter, Is.EqualTo(1));
          }
      
          [Test]
          public void Increment_Twice_ReturnsTwo()
          {
              _counter++;
              _counter++;
              Assert.That(_counter, Is.EqualTo(2));
          }
      }
      ```
      
      ### Classic Assert Instead of Constraint Model
      
      ```csharp
      // WRONG - Classic assert syntax (less readable, deprecated in NUnit 4)
      [Test]
      public void ClassicAsserts()
      {
          Assert.AreEqual(expected, actual);
          Assert.IsTrue(condition);
          Assert.IsNotNull(obj);
          Assert.IsInstanceOf<MyClass>(obj);
          Assert.Throws<Exception>(() => DoSomething());
      }
      
      // CORRECT - Constraint model
      [Test]
      public void ConstraintAsserts()
      {
          Assert.That(actual, Is.EqualTo(expected));
          Assert.That(condition, Is.True);
          Assert.That(obj, Is.Not.Null);
          Assert.That(obj, Is.InstanceOf<MyClass>());
          Assert.That(() => DoSomething(), Throws.TypeOf<Exception>());
      }
      ```
      
      ## Async Anti-Patterns
      
      ### Async Void Tests
      
      ```csharp
      // WRONG - async void cannot be awaited
      [Test]
      public async void GetDataAsync_BadPattern()
      {
          var result = await _service.GetDataAsync();
          Assert.That(result, Is.Not.Null);
          // Test might complete before assertion runs!
      }
      
      // CORRECT - async Task
      [Test]
      public async Task GetDataAsync_GoodPattern()
      {
          var result = await _service.GetDataAsync();
          Assert.That(result, Is.Not.Null);
      }
      ```
      
      ### Blocking on Async Code
      
      ```csharp
      // WRONG - Blocking can cause deadlocks
      [Test]
      public void GetData_Blocking()
      {
          var result = _service.GetDataAsync().Result; // Deadlock risk
          Assert.That(result, Is.Not.Null);
      }
      
      // WRONG - GetAwaiter().GetResult() is just as bad
      [Test]
      public void GetData_GetAwaiter()
      {
          var result = _service.GetDataAsync().GetAwaiter().GetResult();
          Assert.That(result, Is.Not.Null);
      }
      
      // CORRECT - Use async/await
      [Test]
      public async Task GetData_Async()
      {
          var result = await _service.GetDataAsync();
          Assert.That(result, Is.Not.Null);
      }
      ```
      
      ## Setup/Teardown Anti-Patterns
      
      ### Heavy OneTimeSetUp
      
      ```csharp
      // WRONG - Too much work in OneTimeSetUp
      [TestFixture]
      public class HeavySetupTests
      {
          private List<User> _allUsers;
      
          [OneTimeSetUp]
          public async Task OneTimeSetUp()
          {
              // Creates 10000 users - slow and wasteful
              _allUsers = new List<User>();
              for (int i = 0; i < 10000; i++)
              {
                  _allUsers.Add(await _service.CreateUserAsync($"user{i}@test.com"));
              }
          }
      
          [Test]
          public void TestOne()
          {
              // Only needs 1 user
              Assert.That(_allUsers[0].Email, Does.StartWith("user"));
          }
      }
      
      // CORRECT - Create only what each test needs
      [TestFixture]
      public class EfficientSetupTests
      {
          [Test]
          public async Task TestOne()
          {
              var user = await _service.CreateUserAsync("user@test.com");
              Assert.That(user.Email, Does.StartWith("user"));
          }
      }
      ```
      
      ### Missing Teardown for Resources
      
      ```csharp
      // WRONG - Resource leak
      [TestFixture]
      public class LeakyTests
      {
          private HttpClient _client;
      
          [SetUp]
          public void SetUp()
          {
              _client = new HttpClient();
          }
      
          [Test]
          public async Task Test()
          {
              var response = await _client.GetAsync("https://api.example.com");
              // _client is never disposed!
          }
      }
      
      // CORRECT - Proper cleanup
      [TestFixture]
      public class CleanTests
      {
          private HttpClient _client;
      
          [SetUp]
          public void SetUp()
          {
              _client = new HttpClient();
          }
      
          [TearDown]
          public void TearDown()
          {
              _client?.Dispose();
          }
      
          [Test]
          public async Task Test()
          {
              var response = await _client.GetAsync("https://api.example.com");
          }
      }
      ```
      
      ## Assertion Anti-Patterns
      
      ### Swallowing Exceptions
      
      ```csharp
      // WRONG - Exception is caught, test passes incorrectly
      [Test]
      public void BadExceptionHandling()
      {
          try
          {
              _service.DoSomethingThatShouldThrow();
              // Test passes even if exception not thrown!
          }
          catch (Exception)
          {
              // Silently swallowed
          }
      }
      
      // CORRECT - Use Assert.Throws
      [Test]
      public void GoodExceptionHandling()
      {
          Assert.Throws<InvalidOperationException>(
              () => _service.DoSomethingThatShouldThrow());
      }
      ```
      
      ### Assert.Pass() Abuse
      
      ```csharp
      // WRONG - Using Assert.Pass to skip actual assertions
      [Test]
      public void SkippedTest()
      {
          if (!FeatureFlag.IsEnabled)
          {
              Assert.Pass("Feature not enabled"); // Hides untested code
          }
      
          // Never reaches actual test
          var result = _service.NewFeature();
          Assert.That(result, Is.Not.Null);
      }
      
      // CORRECT - Use Assume or Ignore
      [Test]
      public void ConditionalTest()
      {
          Assume.That(FeatureFlag.IsEnabled, "Feature must be enabled");
      
          var result = _service.NewFeature();
          Assert.That(result, Is.Not.Null);
      }
      
      // Or use explicit Ignore
      [Test]
      [Ignore("Feature not yet implemented")]
      public void FutureFeatureTest()
      {
          var result = _service.NewFeature();
          Assert.That(result, Is.Not.Null);
      }
      ```
      
      ### Weak Assertions
      
      ```csharp
      // WRONG - Assertion passes but doesn't verify behavior
      [Test]
      public void WeakAssertion()
      {
          var result = _service.GetUsers();
          Assert.That(result, Is.Not.Null); // Only checks not null
          // Doesn't verify content, count, or correctness
      }
      
      // CORRECT - Specific assertions
      [Test]
      public void StrongAssertion()
      {
          var result = _service.GetActiveUsers();
      
          Assert.Multiple(() =>
          {
              Assert.That(result, Is.Not.Null);
              Assert.That(result, Has.Count.GreaterThan(0));
              Assert.That(result, Is.All.Property("IsActive").True);
          });
      }
      ```
      
      ## Test Data Anti-Patterns
      
      ### Hardcoded Magic Values
      
      ```csharp
      // WRONG - Magic numbers without context
      [Test]
      public void ProcessOrder_MagicNumbers()
      {
          var order = new Order { Quantity = 5, Price = 10.99m };
          var result = _calculator.CalculateTotal(order);
          Assert.That(result, Is.EqualTo(54.95m)); // Where does 54.95 come from?
      }
      
      // CORRECT - Clear calculation or named constants
      [Test]
      public void ProcessOrder_ClearCalculation()
      {
          const int quantity = 5;
          const decimal pricePerUnit = 10.99m;
          const decimal expectedTotal = quantity * pricePerUnit;
      
          var order = new Order { Quantity = quantity, Price = pricePerUnit };
          var result = _calculator.CalculateTotal(order);
      
          Assert.That(result, Is.EqualTo(expectedTotal));
      }
      ```
      
      ### DateTime.Now in Tests
      
      ```csharp
      // WRONG - Non-deterministic test
      [Test]
      public void CreateUser_SetsCreatedAt()
      {
          var user = _service.CreateUser("test@example.com");
          Assert.That(user.CreatedAt, Is.EqualTo(DateTime.Now)); // Race condition!
      }
      
      // CORRECT - Use tolerance or inject time
      [Test]
      public void CreateUser_SetsCreatedAt_WithTolerance()
      {
          var before = DateTime.UtcNow;
          var user = _service.CreateUser("test@example.com");
          var after = DateTime.UtcNow;
      
          Assert.That(user.CreatedAt, Is.InRange(before, after));
      }
      
      // BETTER - Inject clock abstraction
      [Test]
      public void CreateUser_SetsCreatedAt_WithClock()
      {
          var fixedTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
          var clock = Substitute.For<IClock>();
          clock.UtcNow.Returns(fixedTime);
      
          var service = new UserService(clock);
          var user = service.CreateUser("test@example.com");
      
          Assert.That(user.CreatedAt, Is.EqualTo(fixedTime));
      }
      ```
      
      ## Mocking Anti-Patterns
      
      ### Over-Mocking
      
      ```csharp
      // WRONG - Mocking everything, testing nothing
      [Test]
      public void OverMockedTest()
      {
          var mockRepo = Substitute.For<IRepository>();
          var mockLogger = Substitute.For<ILogger>();
          var mockCache = Substitute.For<ICache>();
          var mockValidator = Substitute.For<IValidator>();
      
          mockValidator.Validate(Arg.Any<User>()).Returns(true);
          mockRepo.Save(Arg.Any<User>()).Returns(true);
      
          var service = new UserService(mockRepo, mockLogger, mockCache, mockValidator);
          var result = service.CreateUser("test@example.com");
      
          Assert.That(result, Is.True);
          // What did we actually test? Just that mocks return what we told them to.
      }
      
      // CORRECT - Use real objects where possible
      [Test]
      public void BetterTest()
      {
          var realValidator = new UserValidator();
          var mockRepo = Substitute.For<IRepository>();
      
          var service = new UserService(mockRepo, NullLogger.Instance, realValidator);
          var result = service.CreateUser("test@example.com");
      
          // Now we're actually testing validation + service integration
          mockRepo.Received(1).Save(Arg.Is<User>(u => u.Email == "test@example.com"));
      }
      ```
      
    • patterns.md 7.8 KB
      # NUnit Patterns Reference
      
      ## Test Organization Patterns
      
      ### Fixture Per Feature
      
      ```csharp
      [TestFixture]
      public class UserRegistrationTests
      {
          [Test]
          public void Register_ValidUser_CreatesAccount() { }
      
          [Test]
          public void Register_DuplicateEmail_ReturnsError() { }
      }
      
      [TestFixture]
      public class UserAuthenticationTests
      {
          [Test]
          public void Login_ValidCredentials_ReturnsToken() { }
      
          [Test]
          public void Login_InvalidPassword_ReturnsUnauthorized() { }
      }
      ```
      
      ### Nested Test Fixtures
      
      ```csharp
      [TestFixture]
      public class OrderServiceTests
      {
          [TestFixture]
          public class WhenCreatingOrder
          {
              [Test]
              public void WithValidItems_CreatesOrder() { }
      
              [Test]
              public void WithEmptyCart_ThrowsException() { }
          }
      
          [TestFixture]
          public class WhenCancellingOrder
          {
              [Test]
              public void BeforeShipment_RefundsPayment() { }
      
              [Test]
              public void AfterShipment_ThrowsException() { }
          }
      }
      ```
      
      ## Data-Driven Testing Patterns
      
      ### Values Attribute for Simple Types
      
      ```csharp
      [Test]
      public void IsPositive_VariousNumbers_ReturnsExpected(
          [Values(1, 5, 100, int.MaxValue)] int positive,
          [Values(-1, -100, int.MinValue)] int negative)
      {
          Assert.That(MathHelper.IsPositive(positive), Is.True);
          Assert.That(MathHelper.IsPositive(negative), Is.False);
      }
      
      [Test]
      public void ParseBoolean_VariousStrings(
          [Values("true", "True", "TRUE", "1", "yes")] string trueValue)
      {
          Assert.That(Parser.ParseBoolean(trueValue), Is.True);
      }
      ```
      
      ### Range Attribute
      
      ```csharp
      [Test]
      public void CalculateDiscount_QuantityRange(
          [Range(1, 10)] int quantity)
      {
          var discount = _service.CalculateDiscount(quantity);
          Assert.That(discount, Is.GreaterThanOrEqualTo(0));
      }
      ```
      
      ### Combinatorial and Pairwise
      
      ```csharp
      // Tests all combinations (3 x 3 x 2 = 18 tests)
      [Test, Combinatorial]
      public void TestAllCombinations(
          [Values("small", "medium", "large")] string size,
          [Values("red", "green", "blue")] string color,
          [Values(true, false)] bool express)
      {
          var order = CreateOrder(size, color, express);
          Assert.That(order, Is.Not.Null);
      }
      
      // Tests pairwise combinations (fewer tests, still good coverage)
      [Test, Pairwise]
      public void TestPairwiseCombinations(
          [Values("small", "medium", "large")] string size,
          [Values("red", "green", "blue")] string color,
          [Values(true, false)] bool express)
      {
          var order = CreateOrder(size, color, express);
          Assert.That(order, Is.Not.Null);
      }
      ```
      
      ### TestCaseSource with Named Tests
      
      ```csharp
      private static IEnumerable<TestCaseData> EdgeCaseTestData()
      {
          yield return new TestCaseData(null)
              .SetName("NullInput")
              .SetDescription("Verifies null handling");
      
          yield return new TestCaseData("")
              .SetName("EmptyString")
              .SetDescription("Verifies empty string handling");
      
          yield return new TestCaseData("   ")
              .SetName("WhitespaceOnly")
              .SetDescription("Verifies whitespace handling");
      }
      
      [TestCaseSource(nameof(EdgeCaseTestData))]
      public void Validate_EdgeCases_HandlesGracefully(string input)
      {
          Assert.DoesNotThrow(() => _validator.Validate(input));
      }
      ```
      
      ## Assertion Patterns
      
      ### Multiple Assertions
      
      ```csharp
      [Test]
      public void CreateUser_ValidInput_SetsAllProperties()
      {
          var user = _service.CreateUser("john@example.com", "John Doe");
      
          Assert.Multiple(() =>
          {
              Assert.That(user.Email, Is.EqualTo("john@example.com"));
              Assert.That(user.Name, Is.EqualTo("John Doe"));
              Assert.That(user.CreatedAt, Is.EqualTo(DateTime.UtcNow).Within(1).Seconds);
              Assert.That(user.IsActive, Is.True);
              Assert.That(user.Id, Is.Not.EqualTo(Guid.Empty));
          });
      }
      ```
      
      ### Custom Constraints
      
      ```csharp
      public class ValidEmailConstraint : Constraint
      {
          public override ConstraintResult ApplyTo<TActual>(TActual actual)
          {
              var email = actual as string;
              var isValid = !string.IsNullOrEmpty(email)
                  && email.Contains("@")
                  && email.Contains(".");
      
              return new ConstraintResult(this, actual, isValid);
          }
      
          public override string Description => "a valid email address";
      }
      
      public static class Is2
      {
          public static ValidEmailConstraint ValidEmail => new ValidEmailConstraint();
      }
      
      [Test]
      public void User_Email_IsValid()
      {
          var user = new User { Email = "test@example.com" };
          Assert.That(user.Email, Is2.ValidEmail);
      }
      ```
      
      ### Fluent Collection Assertions
      
      ```csharp
      [Test]
      public void GetActiveUsers_ReturnsCorrectCollection()
      {
          var users = _service.GetActiveUsers();
      
          Assert.That(users, Has.Count.GreaterThan(0)
              .And.All.Property("IsActive").EqualTo(true)
              .And.None.Property("Email").Null
              .And.Some.Property("Role").EqualTo("Admin"));
      }
      
      [Test]
      public void GetProducts_SortedByPrice()
      {
          var products = _service.GetProductsSortedByPrice();
      
          Assert.That(products.Select(p => p.Price), Is.Ordered.Ascending);
      }
      ```
      
      ## Setup and Lifecycle Patterns
      
      ### Dependency Injection in Tests
      
      ```csharp
      [TestFixture]
      public class ServiceTests
      {
          private ServiceProvider _serviceProvider;
          private IMyService _service;
      
          [OneTimeSetUp]
          public void OneTimeSetUp()
          {
              var services = new ServiceCollection();
              services.AddTransient<IMyService, MyService>();
              services.AddSingleton<ILogger, TestLogger>();
              _serviceProvider = services.BuildServiceProvider();
          }
      
          [SetUp]
          public void SetUp()
          {
              _service = _serviceProvider.GetRequiredService<IMyService>();
          }
      
          [OneTimeTearDown]
          public void OneTimeTearDown()
          {
              _serviceProvider?.Dispose();
          }
      }
      ```
      
      ### Test Context Access
      
      ```csharp
      [TestFixture]
      public class ContextAwareTests
      {
          [SetUp]
          public void SetUp()
          {
              var testName = TestContext.CurrentContext.Test.Name;
              var testDirectory = TestContext.CurrentContext.TestDirectory;
      
              Console.WriteLine($"Running: {testName}");
          }
      
          [TearDown]
          public void TearDown()
          {
              var result = TestContext.CurrentContext.Result.Outcome.Status;
      
              if (result == TestStatus.Failed)
              {
                  // Capture screenshot, logs, etc.
                  CaptureFailureArtifacts();
              }
          }
      }
      ```
      
      ## Parallel Execution Patterns
      
      ### Fixture-Level Parallelism
      
      ```csharp
      [TestFixture]
      [Parallelizable(ParallelScope.Fixtures)]
      public class ParallelFixtureTests
      {
          [Test]
          public void Test1() { }
      
          [Test]
          public void Test2() { }
      }
      ```
      
      ### Test-Level Parallelism
      
      ```csharp
      [TestFixture]
      [Parallelizable(ParallelScope.Children)]
      public class ParallelTestsWithinFixture
      {
          // Each test runs in parallel
          [Test]
          public void Test1() { }
      
          [Test]
          public void Test2() { }
      
          [Test]
          public void Test3() { }
      }
      ```
      
      ### Non-Parallelizable Tests
      
      ```csharp
      [TestFixture]
      [NonParallelizable]
      public class SequentialTests
      {
          // Tests that must run sequentially (shared resource, etc.)
      }
      ```
      
      ## Retry and Timeout Patterns
      
      ### Retry Flaky Tests
      
      ```csharp
      [Test]
      [Retry(3)]
      public void FlakyNetworkTest()
      {
          // Will retry up to 3 times if fails
          var result = _httpClient.GetAsync("https://api.example.com").Result;
          Assert.That(result.IsSuccessStatusCode, Is.True);
      }
      ```
      
      ### Test Timeout
      
      ```csharp
      [Test]
      [Timeout(5000)] // 5 seconds
      public void LongRunningTest()
      {
          // Test will fail if takes longer than 5 seconds
          _service.ProcessLargeDataSet();
      }
      ```
      
      ## Theory Tests (NUnit 4)
      
      ```csharp
      [TestFixture]
      public class TheoryTests
      {
          [Theory]
          public void Sqrt_PositiveNumber_ReturnsPositive(double value)
          {
              Assume.That(value > 0);
              var result = Math.Sqrt(value);
              Assert.That(result, Is.GreaterThan(0));
          }
      
          [Datapoint] public double Zero = 0;
          [Datapoint] public double One = 1;
          [Datapoint] public double Pi = Math.PI;
          [Datapoint] public double Negative = -1;
      }
      ```
      
  • SKILL.md 9.3 KB
    ---
    name: dotnet-nunit
    version: "1.0.0"
    category: "Testing"
    description: "Write, run, or repair .NET tests that use NUnit. Use when a repo uses `NUnit`, `[Test]`, `[TestCase]`, `[TestFixture]`, or NUnit3TestAdapter for VSTest or Microsoft.Testing.Platform execution."
    compatibility: "Requires NUnit 3.x or 4.x packages and appropriate test adapter."
    ---
    
    # NUnit Testing
    
    ## Trigger On
    
    - writing or reviewing NUnit tests
    - using `[Test]`, `[TestCase]`, `[TestFixture]`, `[SetUp]`, `[TearDown]` attributes
    - configuring NUnit3TestAdapter or NUnit.Analyzers
    - migrating between NUnit versions
    - integrating NUnit with CI pipelines
    
    ## Documentation
    
    - [NUnit Documentation](https://docs.nunit.org/)
    - [NUnit GitHub](https://github.com/nunit/nunit)
    - [NUnit3TestAdapter](https://github.com/nunit/nunit3-vs-adapter)
    - [NUnit Analyzers](https://github.com/nunit/nunit.analyzers)
    
    ### References
    
    - [patterns.md](references/patterns.md) — Test patterns, assertions, parameterized tests, lifecycle
    - [anti-patterns.md](references/anti-patterns.md) — Common NUnit mistakes and fixes
    
    ## Package Selection
    
    | Package | Purpose |
    |---------|---------|
    | `NUnit` | Core testing framework |
    | `NUnit3TestAdapter` | VSTest adapter for `dotnet test` |
    | `NUnit.Analyzers` | Roslyn analyzers for NUnit best practices |
    | `Microsoft.NET.Test.Sdk` | Required for test discovery |
    
    ## Project Setup
    
    ```xml
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <IsPackable>false</IsPackable>
        <IsTestProject>true</IsTestProject>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
        <PackageReference Include="NUnit" Version="4.*" />
        <PackageReference Include="NUnit3TestAdapter" Version="4.*" />
        <PackageReference Include="NUnit.Analyzers" Version="4.*">
          <PrivateAssets>all</PrivateAssets>
          <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
        </PackageReference>
      </ItemGroup>
    </Project>
    ```
    
    ## Test Patterns
    
    ### Basic Test Structure
    
    ```csharp
    [TestFixture]
    public class CalculatorTests
    {
        private Calculator _calculator;
    
        [SetUp]
        public void SetUp()
        {
            _calculator = new Calculator();
        }
    
        [TearDown]
        public void TearDown()
        {
            _calculator?.Dispose();
        }
    
        [Test]
        public void Add_TwoPositiveNumbers_ReturnsSum()
        {
            var result = _calculator.Add(2, 3);
    
            Assert.That(result, Is.EqualTo(5));
        }
    
        [Test]
        public void Divide_ByZero_ThrowsException()
        {
            Assert.Throws<DivideByZeroException>(() => _calculator.Divide(10, 0));
        }
    }
    ```
    
    ### Parameterized Tests with TestCase
    
    ```csharp
    [TestFixture]
    public class ValidationTests
    {
        [TestCase("", false)]
        [TestCase("a", false)]
        [TestCase("ab", false)]
        [TestCase("abc", true)]
        [TestCase("valid@email.com", true)]
        public void IsValid_VariousInputs_ReturnsExpected(string input, bool expected)
        {
            var result = Validator.IsValid(input);
    
            Assert.That(result, Is.EqualTo(expected));
        }
    
        [TestCase(1, 2, ExpectedResult = 3)]
        [TestCase(-1, 1, ExpectedResult = 0)]
        [TestCase(100, 200, ExpectedResult = 300)]
        public int Add_TestCases_ReturnsExpectedResult(int a, int b)
        {
            return _calculator.Add(a, b);
        }
    }
    ```
    
    ### TestCaseSource for Complex Data
    
    ```csharp
    [TestFixture]
    public class OrderTests
    {
        private static IEnumerable<TestCaseData> OrderTestCases()
        {
            yield return new TestCaseData(
                new Order { Items = new[] { new Item { Price = 10 }, new Item { Price = 20 } } },
                30m
            ).SetName("TwoItems_CalculatesTotal");
    
            yield return new TestCaseData(
                new Order { Items = Array.Empty<Item>() },
                0m
            ).SetName("EmptyOrder_ReturnsZero");
    
            yield return new TestCaseData(
                new Order { Items = new[] { new Item { Price = 100 } }, DiscountPercent = 10 },
                90m
            ).SetName("WithDiscount_AppliesDiscount");
        }
    
        [TestCaseSource(nameof(OrderTestCases))]
        public void CalculateTotal_VariousOrders_ReturnsExpected(Order order, decimal expected)
        {
            var result = order.CalculateTotal();
    
            Assert.That(result, Is.EqualTo(expected));
        }
    }
    ```
    
    ### Constraint-Based Assertions
    
    ```csharp
    [Test]
    public void AssertionExamples()
    {
        // Equality
        Assert.That(actual, Is.EqualTo(expected));
        Assert.That(actual, Is.Not.EqualTo(other));
    
        // Comparison
        Assert.That(value, Is.GreaterThan(5));
        Assert.That(value, Is.LessThanOrEqualTo(10));
        Assert.That(value, Is.InRange(1, 100));
    
        // String
        Assert.That(str, Does.StartWith("Hello"));
        Assert.That(str, Does.Contain("world"));
        Assert.That(str, Does.Match(@"\d{3}-\d{4}"));
        Assert.That(str, Is.EqualTo("HELLO").IgnoreCase);
    
        // Collection
        Assert.That(list, Has.Count.EqualTo(5));
        Assert.That(list, Contains.Item("expected"));
        Assert.That(list, Is.All.GreaterThan(0));
        Assert.That(list, Is.Unique);
        Assert.That(list, Is.Ordered);
        Assert.That(list, Has.Exactly(3).Items.GreaterThan(10));
    
        // Type
        Assert.That(obj, Is.InstanceOf<MyClass>());
        Assert.That(obj, Is.AssignableTo<IMyInterface>());
    
        // Null
        Assert.That(obj, Is.Null);
        Assert.That(obj, Is.Not.Null);
    
        // Boolean
        Assert.That(condition, Is.True);
        Assert.That(condition, Is.False);
    
        // Exception
        Assert.That(() => DoSomething(), Throws.TypeOf<InvalidOperationException>());
        Assert.That(() => DoSomething(), Throws.Exception.With.Message.Contains("error"));
    
        // Async
        Assert.That(async () => await DoAsync(), Throws.Nothing);
    }
    ```
    
    ### Async Test Support
    
    ```csharp
    [TestFixture]
    public class AsyncServiceTests
    {
        private IAsyncService _service;
    
        [SetUp]
        public void SetUp()
        {
            _service = new AsyncService();
        }
    
        [Test]
        public async Task GetDataAsync_ValidId_ReturnsData()
        {
            var result = await _service.GetDataAsync(1);
    
            Assert.That(result, Is.Not.Null);
            Assert.That(result.Id, Is.EqualTo(1));
        }
    
        [Test]
        public void GetDataAsync_InvalidId_ThrowsException()
        {
            Assert.ThrowsAsync<NotFoundException>(
                async () => await _service.GetDataAsync(-1));
        }
    
        [Test]
        public async Task ProcessAsync_CancellationRequested_ThrowsOperationCanceled()
        {
            using var cts = new CancellationTokenSource();
            cts.Cancel();
    
            Assert.ThrowsAsync<OperationCanceledException>(
                async () => await _service.ProcessAsync(cts.Token));
        }
    }
    ```
    
    ### OneTimeSetUp and OneTimeTearDown
    
    ```csharp
    [TestFixture]
    public class IntegrationTests
    {
        private static TestServer _server;
        private HttpClient _client;
    
        [OneTimeSetUp]
        public void OneTimeSetUp()
        {
            // Runs once before all tests in fixture
            _server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
        }
    
        [OneTimeTearDown]
        public void OneTimeTearDown()
        {
            // Runs once after all tests in fixture
            _server?.Dispose();
        }
    
        [SetUp]
        public void SetUp()
        {
            // Runs before each test
            _client = _server.CreateClient();
        }
    
        [TearDown]
        public void TearDown()
        {
            // Runs after each test
            _client?.Dispose();
        }
    
        [Test]
        public async Task GetEndpoint_ReturnsSuccess()
        {
            var response = await _client.GetAsync("/api/data");
            Assert.That(response.IsSuccessStatusCode, Is.True);
        }
    }
    ```
    
    ## Categories and Filtering
    
    ```csharp
    [TestFixture]
    [Category("Integration")]
    public class DatabaseTests
    {
        [Test]
        [Category("Slow")]
        public void SlowDatabaseTest() { }
    
        [Test]
        [Category("Fast")]
        public void FastDatabaseTest() { }
    }
    
    // Run specific categories:
    // dotnet test --filter "Category=Fast"
    // dotnet test --filter "Category!=Slow"
    ```
    
    ## Anti-Patterns to Avoid
    
    | Anti-Pattern | Why It's Bad | Better Approach |
    |--------------|--------------|-----------------|
    | Multiple asserts without clear purpose | Hard to identify which assertion failed | One logical assertion per test or use `Assert.Multiple` |
    | Test interdependence | Tests fail unpredictably | Each test should be independent |
    | Hardcoded test data | Brittle tests | Use `TestCase` or `TestCaseSource` |
    | Testing implementation details | Breaks on refactoring | Test behavior, not internals |
    | Missing `[SetUp]`/`[TearDown]` cleanup | Resource leaks | Always clean up resources |
    | Classic Assert syntax | Less readable | Use constraint model (`Assert.That`) |
    
    ## Running Tests
    
    ```bash
    # Run all tests
    dotnet test
    
    # Run with verbosity
    dotnet test --logger "console;verbosity=detailed"
    
    # Filter by name
    dotnet test --filter "FullyQualifiedName~CalculatorTests"
    
    # Filter by category
    dotnet test --filter "Category=Unit"
    
    # Run in parallel
    dotnet test -- NUnit.NumberOfTestWorkers=4
    ```
    
    ## Deliver
    
    - NUnit tests following the Arrange-Act-Assert pattern
    - Parameterized tests with `[TestCase]` and `[TestCaseSource]`
    - Constraint-based assertions with `Assert.That`
    - Proper test lifecycle management
    
    ## Validate
    
    - Tests are independent and isolated
    - No hardcoded test data where parameterization is appropriate
    - Async tests use `async Task` not `async void`
    - Resources are properly disposed in `[TearDown]` or `[OneTimeTearDown]`
    - NUnit.Analyzers enabled to catch common issues
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related