Test Requirements
Coverage Requirement
40% line coverage is enforced in CI. Every PR must maintain or improve coverage. Failing builds will be rejected. Check coverage locally:Test Framework
xUnit (2.5.3) with NSubstitute (5.3.0) for mocking. Fromtests/Intune.Commander.Core.Tests/Intune.Commander.Core.Tests.csproj:
Test Structure
Project Organization
src/Intune.Commander.Core/.
Naming Conventions
Test Class:{ClassUnderTest}Tests
Test Method:
{MethodName}_{Scenario}_{ExpectedBehavior}
Test File:
{ClassUnderTest}Tests.cs
Unit Tests
Basic Unit Test Structure
// Arrange- Set up test data// Act- Execute the method under test// Assert- Verify expected outcomestry/finally- Clean up resources (temp files, directories)
Testing with NSubstitute
Creating Mocks
Return Values
Argument Capture
Call Verification
Testing Graph Services
Problem:GraphServiceClient is sealed and cannot be mocked.
Solution: Use reflection-based contract tests to verify interface conformance.
Service Contract Tests
- Service implements its interface
- All methods accept
CancellationToken - All async methods return
TaskorTask<T> - Constructor accepts
GraphServiceClient
GraphServiceClient, we verify the service contract instead. Integration tests will verify actual behavior.
Theory Tests (Parameterized)
[Theory] when:
- Testing multiple input/output combinations
- Validating behavior across different scenarios
- Reducing test duplication
Integration Tests
Integration Test Setup
Integration tests run against a live Azure tenant with a real Graph API connection. Requirements:- Test tenant (non-production)
- App registration with required permissions
- Environment variables:
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRET
docs/GRAPH-PERMISSIONS.md for the complete list.
Base Class: GraphIntegrationTestBase
Writing Integration Tests
Read-Only Tests (Safe)
CRUD Tests (With Cleanup)
- Prefix created objects with
IntTest_AutoCleanup_ - Always clean up in
finallyblock - Use unique test IDs (GUID) to avoid conflicts
- Best-effort cleanup (catch and ignore cleanup errors)
Integration Test Tagging
ALWAYS tag integration tests:Running Tests
All Tests (Unit + Integration)
Unit Tests Only
Integration Tests Only
Specific Test Class
Specific Test Method
With Code Coverage
With Coverage Threshold
CI/CD Testing
CI — Test & Coverage
File:.github/workflows/ci-test.yml
Trigger: All pushes + PRs to main
What it does:
- Restores dependencies
- Builds solution
- Runs unit tests (excludes integration tests)
- Enforces 40% line coverage
- Uploads coverage report as artifact
CI — Integration Tests
File:.github/workflows/ci-integration.yml
Trigger: Push/PR to main + manual dispatch
What it does:
- Restores dependencies
- Builds solution
- Runs integration tests against live tenant
- Uses repository secrets for credentials
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRET
Test Coverage Best Practices
What to Test
High Priority:- Public APIs and interfaces
- Business logic and algorithms
- Error handling and edge cases
- Data validation and transformation
- Internal helper methods (if complex)
- Configuration and setup code
- Serialization/deserialization
- Simple property getters/setters
- Auto-generated code (MVVM source generators)
- UI code (ViewModels are tested, but not Views)
Coverage Anti-Patterns
Don’t:- Write tests just to hit coverage numbers
- Test framework code (e.g., testing that
ObservableObjectworks) - Test trivial code (e.g.,
return _value;)
- Focus on meaningful behavior
- Test edge cases and error paths
- Verify business logic correctness
Example: Good vs Bad Tests
Bad (just hitting coverage):Related Documentation
- Architecture Overview - Testing strategy overview
- Technology Stack - Test framework dependencies
- Services - Service testing patterns
- Contributing - PR testing requirements
- Building - Running tests locally