| name | write-unit-tests |
| description | Write comprehensive unit tests with proper setup, assertions, and coverage of happy paths, edge cases, and error scenarios |
| auto_invoke | true |
Write Unit Tests
Guide for writing effective unit tests that verify behavior and catch regressions.
When to Use
- Testing pure logic and business rules
- Verifying individual components in isolation
- Testing edge cases and error conditions
- TDD red-green-refactor cycle
Test Structure (Arrange-Act-Assert)
[Fact]
public void MethodName_Scenario_ExpectedBehavior()
{
var dependency = Substitute.For<IDependency>();
dependency.GetData().Returns(expectedData);
var sut = new SystemUnderTest(dependency);
var result = sut.MethodUnderTest(input);
result.Should().Be(expectedValue);
dependency.Received(1).GetData();
}
Test Naming
Use descriptive names that explain what's being tested:
MethodName_Scenario_ExpectedBehavior
MethodName_WhenCondition_ShouldDoSomething
MethodName_GivenInput_ReturnsExpectedOutput
Examples:
Calculate_WithNegativeNumber_ThrowsArgumentException
ProcessOrder_WhenInventoryInsufficient_ReturnsFailureResult
FormatDate_GivenNull_ReturnsEmptyString
Test Coverage
1. Happy Path
- Normal, expected inputs
- Successful execution
- Expected outputs
2. Edge Cases
- Boundary values (min, max, zero, empty)
- Null inputs
- Empty collections
- Special characters
3. Error Scenarios
- Invalid inputs
- Exceptions
- Validation failures
- Constraint violations
Mocking & Isolation
- Use test doubles for dependencies
- Mock external services, databases, APIs
- Keep tests fast and deterministic
- Don't mock what you don't own (value objects, DTOs)
var repository = Substitute.For<IRepository>();
var dto = Substitute.For<DataTransferObject>();
Assertions
Use fluent assertions for clarity:
result.Should().Be(expected);
result.Should().BeEquivalentTo(expected);
collection.Should().HaveCount(3);
action.Should().Throw<ArgumentException>()
.WithMessage("*invalid*");
Assert.Equal(expected, result);
Assert.True(result != null);
Best Practices
- One assertion per test - Test one thing at a time
- Fast execution - No I/O, no sleeps, no real dependencies
- Deterministic - Same inputs always produce same results
- Independent - Tests don't depend on each other
- Readable - Clear setup, action, and verification
- Maintainable - Easy to update when requirements change
Common Patterns
Testing Exceptions
[Fact]
public void Method_InvalidInput_ThrowsException()
{
var sut = new SystemUnderTest();
var action = () => sut.Method(invalidInput);
action.Should().Throw<ArgumentException>()
.WithMessage("*parameter*");
}
Testing Async Methods
[Fact]
public async Task MethodAsync_Scenario_ExpectedBehavior()
{
var sut = new SystemUnderTest();
var result = await sut.MethodAsync();
result.Should().NotBeNull();
}
Theory Tests (Multiple Inputs)
[Theory]
[InlineData(0, 0)]
[InlineData(1, 1)]
[InlineData(-1, 1)]
public void Abs_VariousInputs_ReturnsAbsoluteValue(int input, int expected)
{
var result = Math.Abs(input);
result.Should().Be(expected);
}
Common Pitfalls
- ā Testing implementation details
- ā Tests that depend on execution order
- ā Flaky tests (random, time-dependent)
- ā Testing too much in one test
- ā Not testing error cases
- ā Using real dependencies (databases, files, network)
Test Organization
Tests/
āāā Unit/
ā āāā Services/
ā ā āāā OrderServiceTests.cs
ā āāā Handlers/
ā ā āāā CreateOrderHandlerTests.cs
ā āāā Validators/
ā āāā OrderValidatorTests.cs
Checklist