--- 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: ```csharp [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: ```csharp // 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: ```csharp [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) 1. **Statement Coverage (Line Coverage)**: 70-80% minimum - Every executable line executed at least once - Does NOT guarantee logic errors are caught 2. **Branch Coverage (Decision Coverage)**: 80%+ target - Every if/else condition evaluated true AND false - Stronger than statement coverage 3. **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: ```csharp // Mock a dependency var mockRepository = new Mock(); mockRepository .Setup(r => r.GetUser(It.IsAny())) .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: ```csharp // 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 ```csharp [Fact] public void Process_WithInvalidInput_ThrowsArgumentException() { var service = new Service(); // Assert that exception is thrown var ex = Assert.Throws( () => 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( () => service.ProcessAsync(null) ); } ``` ### Exception Validation Strategy Always verify: 1. **Correct exception type** is thrown 2. **Message contains actionable information** 3. **Inner exception** (if present) provides context 4. **Both happy path AND error paths** are tested ## Parameterized Testing (xUnit) Use `[Theory]` and `[InlineData]` to test multiple inputs: ```csharp [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 GetTestData() { yield return new object[] { 1, new { Value = 1 } }; yield return new object[] { 2, new { Value = 2 } }; } ``` ## Test Fixtures & Setup ### Fixture Management ```csharp // 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: ```csharp 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 ```csharp // 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: ```csharp [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 ```csharp [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 ```csharp [Fact] public async Task Operation_WithCancellation_ThrowsOperationCanceledException() { var cts = new CancellationTokenSource(); cts.CancelAfter(100); await Assert.ThrowsAsync( () => _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 concepts - `references/test-design-techniques.md` - Equivalence partitioning, boundary analysis, decision tables - `references/xunit-framework-guide.md` - xUnit framework features and best practices - `references/mocking-patterns.md` - Moq library patterns and anti-patterns - `references/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 testing - `user-repository.tests.cs` - Data access with mocking - `authorization-service.tests.cs` - Business logic with decision tables - `async-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