name: csharp-unit-testing description: Expert-level C# unit testing skill based on ISTQB standards and best practices. Use this skill when creating, reviewing, or refactoring unit tests for C# applications (.NET/ASP.NET Core). Applies test design techniques, coverage strategies, and quality assurance principles from ISTQB Foundation and Advanced levels. license: Complete terms in LICENSE.txt
C# Unit Testing Skill (ISTQB-Compliant)
This skill provides comprehensive guidance for creating high-quality unit tests in C# following ISTQB (International Software Testing Qualifications Board) standards and industry best practices.
When to Use This Skill
Use this skill when you need to:
- Create unit tests for C# classes, methods, and business logic
- Review test quality for compliance with ISTQB standards and best practices
- Design test cases using systematic test design techniques (equivalence partitioning, boundary value analysis, etc.)
- Refactor existing tests to improve maintainability, coverage, and clarity
- Establish test strategies for ensuring comprehensive coverage
- Write integration tests that bridge unit and system testing levels
- Debug failing tests and improve test reliability
- Set up test fixtures, mocks, and test data properly
ISTQB Testing Levels
Unit tests primarily address the Component Testing (unit) level, which verifies:
- Individual units of code work as specified
- Interactions between closely related components
- Error handling and boundary conditions
- Performance characteristics at the component level
Core Testing Principles
1. Test Purpose & Scope
Every unit test must have a single, clear purpose. Identify:
- What is being tested (specific method/behavior)
- Why it matters (business value, risk mitigation)
- How it will be validated (assertions, exceptions)
2. Naming Convention (AAA + Clear Intent)
[Public Method Name]_[Scenario]_[Expected Result]
Example: CalculateDiscount_WithValidPercentage_ReturnsCorrectAmount
Follow these naming rules:
- Use descriptive names (3-5 words minimum)
- Avoid cryptic abbreviations
- Express the expected outcome, not implementation details
- Use PascalCase consistently
3. Test Structure (Arrange-Act-Assert)
Every test follows the AAA pattern:
[Fact]
public void Method_Scenario_ExpectedResult()
{
// Arrange: Set up test data and dependencies
var service = new CalculationService();
var input = 100m;
// Act: Execute the method being tested
var result = service.CalculateDiscount(input, 0.10m);
// Assert: Verify the outcome
Assert.Equal(90m, result);
}
4. Test Characteristics (ISTQB Standards)
Each test should be:
| Characteristic | Description |
|---|---|
| Isolated | No dependencies on other tests; tests can run in any order |
| Deterministic | Always passes or fails consistently (no random/time-dependent behavior) |
| Focused | Tests one logical concept; multiple assertions only if verifying single behavior |
| Readable | Clear intent without requiring code navigation |
| Fast | Executes in milliseconds; uses mocks for external dependencies |
| Maintainable | Uses DRY principle; minimizes setup complexity |
| Reliable | Fails only when actual behavior changes, not due to environmental factors |
Test Design Techniques (ISTQB)
Equivalence Partitioning
Divide input domain into classes where behavior should be identical:
// For age-based eligibility
// Partition 1: < 18 (invalid)
// Partition 2: 18-65 (eligible)
// Partition 3: > 65 (senior eligible)
Create tests for one valid representative and one invalid representative per partition.
Boundary Value Analysis
Test at partition boundaries ± 1:
[Theory]
[InlineData(17)] // Just below boundary
[InlineData(18)] // Boundary
[InlineData(19)] // Just above boundary
public void IsEligible_AgeAtBoundary_ReturnsExpected(int age)
{
// Test implementation
}
State Transition Testing
For objects with state machines, test valid transitions:
State A --[Valid Trigger]--> State B --[Valid Trigger]--> State C
Invalid transitions from each state should also be tested
Decision Table Testing
For complex logic with multiple conditions:
| Condition A | Condition B | Expected Result |
|---|---|---|
| True | True | X |
| True | False | Y |
| False | True | Z |
| False | False | W |
Create a test for each row.
Test Coverage Strategy
Coverage Levels (Hierarchy)
-
Statement Coverage (Line Coverage): 70-80% minimum
- Every executable line executed at least once
- Does NOT guarantee logic errors are caught
-
Branch Coverage (Decision Coverage): 80%+ target
- Every if/else condition evaluated true AND false
- Stronger than statement coverage
-
Path Coverage: Target for critical paths
- Every possible execution path tested
- Often infeasible for complex code (combinatorial explosion)
Coverage Guidelines
- Aim for 80%+ overall coverage for business logic
- 100% coverage for:
- Security-critical operations
- Financial calculations
- Authorization/authentication logic
- Core business rules
- Lower coverage acceptable for:
- UI boilerplate
- Framework-generated code
- Trivial getters/setters
- Logging-only methods
Mocking & Test Doubles (ISTQB)
Test Double Types
| Type | Purpose | When to Use |
|---|---|---|
| Stub | Returns predetermined values | External API, database queries |
| Mock | Verifies method calls and arguments | Dependency verification |
| Spy | Records interactions while calling real method | Partial mocking |
| Fake | Lightweight working implementation | In-memory database, file system |
Use Moq library for creating test doubles:
// Mock a dependency
var mockRepository = new Mock<IUserRepository>();
mockRepository
.Setup(r => r.GetUser(It.IsAny<int>()))
.ReturnsAsync(new User { Id = 1, Name = "Test" });
// Verify it was called
mockRepository.Verify(r => r.GetUser(1), Times.Once);
Dependency Injection in Tests
Always inject dependencies to enable testing:
// Bad: Hard to test
public class UserService
{
private readonly IRepository _repo = new Repository();
}
// Good: Testable with mock injection
public class UserService
{
private readonly IRepository _repo;
public UserService(IRepository repo) => _repo = repo;
}
Exception Testing
Testing Expected Exceptions
[Fact]
public void Process_WithInvalidInput_ThrowsArgumentException()
{
var service = new Service();
// Assert that exception is thrown
var ex = Assert.Throws<ArgumentException>(
() => service.Process(null)
);
// Verify exception details
Assert.Contains("required", ex.Message, StringComparison.OrdinalIgnoreCase);
}
// For async operations
[Fact]
public async Task ProcessAsync_WithInvalidInput_ThrowsArgumentException()
{
var service = new Service();
await Assert.ThrowsAsync<ArgumentException>(
() => service.ProcessAsync(null)
);
}
Exception Validation Strategy
Always verify:
- Correct exception type is thrown
- Message contains actionable information
- Inner exception (if present) provides context
- Both happy path AND error paths are tested
Parameterized Testing (xUnit)
Use [Theory] and [InlineData] to test multiple inputs:
[Theory]
[InlineData(0, 0)] // Boundary
[InlineData(1, 1)] // Single item
[InlineData(100, 100)] // Large value
[InlineData(-1, null)] // Invalid
public void Calculate_WithVariousInputs_ReturnsExpected(int input, int? expected)
{
var result = Calculator.Process(input);
Assert.Equal(expected, result);
}
// Or using MemberData for complex data
[Theory]
[MemberData(nameof(GetTestData))]
public void Test_WithComplexData(int input, object expected)
{
// Test implementation
}
public static IEnumerable<object[]> GetTestData()
{
yield return new object[] { 1, new { Value = 1 } };
yield return new object[] { 2, new { Value = 2 } };
}
Test Fixtures & Setup
Fixture Management
// Use IAsyncLifetime for async setup/cleanup
public class TestClass : IAsyncLifetime
{
private readonly IRepository _repository;
public TestClass()
{
_repository = new TestRepository();
}
public async Task InitializeAsync()
{
// Async setup (e.g., database initialization)
await _repository.InitializeAsync();
}
public async Task DisposeAsync()
{
// Cleanup
await _repository.ClearAsync();
}
[Fact]
public async Task Test_WithAsyncSetup()
{
// Test body
}
}
Test Data Builders
Create reusable test data:
public class UserBuilder
{
private string _name = "TestUser";
private int _age = 25;
public UserBuilder WithName(string name)
{
_name = name;
return this;
}
public UserBuilder WithAge(int age)
{
_age = age;
return this;
}
public User Build() => new User { Name = _name, Age = _age };
}
// Usage
var user = new UserBuilder()
.WithAge(30)
.Build();
Assertion Best Practices
Use Specific Assertions
// Bad: Generic
Assert.True(result == expected);
// Good: Specific
Assert.Equal(expected, result);
Assert.NotNull(user);
Assert.Empty(list);
Assert.Contains("text", result);
Multiple Assertions (When Appropriate)
Only when testing a single cohesive behavior:
[Fact]
public void User_Created_IsInitializedCorrectly()
{
var user = new User("John", 30);
// All assertions verify the SAME behavior: proper initialization
Assert.Equal("John", user.Name);
Assert.Equal(30, user.Age);
Assert.False(user.IsActive);
}
Async Testing
Testing Async Methods
[Fact]
public async Task GetUser_WithValidId_ReturnsUser()
{
// Setup
var repository = new UserRepository();
// Act
var user = await repository.GetUserAsync(1);
// Assert
Assert.NotNull(user);
Assert.Equal(1, user.Id);
}
// Avoid .Result (causes deadlocks)
[Fact]
public void BadAsyncTest()
{
// WRONG: This can deadlock
var result = _service.GetDataAsync().Result;
}
Testing Cancellation
[Fact]
public async Task Operation_WithCancellation_ThrowsOperationCanceledException()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(100);
await Assert.ThrowsAsync<OperationCanceledException>(
() => _service.LongOperationAsync(cts.Token)
);
}
Code Review Checklist for Unit Tests
When reviewing C# unit tests, verify:
- Test name clearly describes scenario and expected result
- AAA structure is evident (Arrange, Act, Assert)
- Single responsibility (one logical concept per test)
- No test interdependencies (can run in any order, isolated)
- Appropriate use of mocks (external dependencies mocked, internal logic real)
- Assertions are specific (not generic True/False checks)
- Exception cases tested (both happy path and error paths)
- Boundary values tested (if applicable via equivalence partitioning)
- Setup/teardown is minimal (only essential initialization)
- No hardcoded delays (no Thread.Sleep or Task.Delay)
- Async/await used correctly (no .Result/.Wait())
- Test data is realistic (represents real-world scenarios)
- Coverage is adequate (80%+ for business logic)
- Tests are deterministic (no time/random dependencies)
- Documentation is clear (comments for non-obvious logic)
Common Pitfalls to Avoid
| Pitfall | Issue | Solution |
|---|---|---|
| Magic values | Hard to understand test intent | Use named variables: const int ValidAge = 25; |
| Test interdependency | One failing test breaks others | Ensure each test is completely independent |
| Over-mocking | Tests pass but integration fails | Mock only external dependencies, test real logic |
| Brittle assertions | Fails on unrelated changes | Assert on behavior, not implementation details |
| Ignored tests | Dead code accumulates | Remove or fix; never use [Ignore] long-term |
| Excessive setup | Tests hard to understand | Extract to helper methods or builders |
| Testing implementation | Tests break during refactoring | Test behavior/contracts, not internal details |
| No negative tests | Missing error scenarios | Always test error cases and boundaries |
| Async antipatterns | Deadlocks or race conditions | Use async/await, avoid .Result/.Wait() |
Links to Reference Files
For detailed guidance on specific topics, see:
references/istqb-standards-summary.md- ISTQB Foundation conceptsreferences/test-design-techniques.md- Equivalence partitioning, boundary analysis, decision tablesreferences/xunit-framework-guide.md- xUnit framework features and best practicesreferences/mocking-patterns.md- Moq library patterns and anti-patternsreferences/async-testing-guide.md- Comprehensive async/await testing strategies
Example Tests
See examples/ directory for annotated example tests:
calculator-service.tests.cs- Basic arithmetic with boundary testinguser-repository.tests.cs- Data access with mockingauthorization-service.tests.cs- Business logic with decision tablesasync-api-service.tests.cs- Async operations and exception handling
Keywords
unit testing, C#, .NET, ASP.NET Core, xUnit, Moq, ISTQB, test design, TDD, test coverage, mocking, assertions, async testing