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.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-nunit-bfa4ebd.zip · 8 KB
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
- patterns.md — Test patterns, assertions, parameterized tests, lifecycle
- 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
<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 Tasknotasync 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.
Reviews (0)
No reviews yet.
No comments yet.