Claude-skill-registry csharp-unit-testing
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/csharp-unit-testing" ~/.claude/skills/majiayu000-claude-skill-registry-csharp-unit-testing && rm -rf "$T"
skills/data/csharp-unit-testing/SKILL.mdC# 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: |
| 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:
- ISTQB Foundation conceptsreferences/istqb-standards-summary.md
- Equivalence partitioning, boundary analysis, decision tablesreferences/test-design-techniques.md
- xUnit framework features and best practicesreferences/xunit-framework-guide.md
- Moq library patterns and anti-patternsreferences/mocking-patterns.md
- Comprehensive async/await testing strategiesreferences/async-testing-guide.md
Example Tests
See
examples/ directory for annotated example tests:
- Basic arithmetic with boundary testingcalculator-service.tests.cs
- Data access with mockinguser-repository.tests.cs
- Business logic with decision tablesauthorization-service.tests.cs
- Async operations and exception handlingasync-api-service.tests.cs
Keywords
unit testing, C#, .NET, ASP.NET Core, xUnit, Moq, ISTQB, test design, TDD, test coverage, mocking, assertions, async testing