
csharp-unit-testing
by Takas0522
GitHubのChangeLogで発表されたあれこれを試すためのリポジトリ
SKILL.md
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
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon