
Nunit
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
nunit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- nunit
- AI & Agent Building
- AI-coding skill
Nunit by the numbers
- 17 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,861 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill nunitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
Workflow
1. Detect whether the project uses NUnit 3.x or 4.x and which runner path is active: VSTest, Microsoft.Testing.Platform, IDE runner, or CI wrapper. 2. Keep test fixtures small, prefer focused assertions with Assert.That, and use TestCase or TestCaseSource only when parameterization improves signal. 3. Add NUnit3TestAdapter, Microsoft.NET.Test.Sdk, and NUnit.Analyzers when CLI discovery or analyzer coverage is missing. 4. Validate with the repo's real test command before changing assertion style or lifecycle hooks.
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=4Deliver
- 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
{
"version": "1.0.0",
"category": "Testing",
"packages": [
"NUnit",
"NUnit3TestAdapter"
]
}
NUnit Anti-Patterns Reference
Test Structure Anti-Patterns
Multiple Unrelated Assertions
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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"));
}NUnit Patterns Reference
Test Organization Patterns
Fixture Per Feature
[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
[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
[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
[Test]
public void CalculateDiscount_QuantityRange(
[Range(1, 10)] int quantity)
{
var discount = _service.CalculateDiscount(quantity);
Assert.That(discount, Is.GreaterThanOrEqualTo(0));
}Combinatorial and Pairwise
// 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
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
[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
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
[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
[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
[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
[TestFixture]
[Parallelizable(ParallelScope.Fixtures)]
public class ParallelFixtureTests
{
[Test]
public void Test1() { }
[Test]
public void Test2() { }
}Test-Level Parallelism
[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
[TestFixture]
[NonParallelizable]
public class SequentialTests
{
// Tests that must run sequentially (shared resource, etc.)
}Retry and Timeout Patterns
Retry Flaky Tests
[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
[Test]
[Timeout(5000)] // 5 seconds
public void LongRunningTest()
{
// Test will fail if takes longer than 5 seconds
_service.ProcessLargeDataSet();
}Theory Tests (NUnit 4)
[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;
}