# StellaOps.TestKit Usage Guide
**Version:** 1.0
**Status:** Pilot Release (Wave 4 Complete)
**Audience:** StellaOps developers writing unit, integration, and contract tests
---
## Overview
`StellaOps.TestKit` provides deterministic testing infrastructure for StellaOps modules. It eliminates flaky tests, provides reproducible test primitives, and standardizes fixtures for integration testing.
### Key Features
- **Deterministic Time**: Freeze and advance time for reproducible tests
- **Deterministic Random**: Seeded random number generation
- **Canonical JSON Assertions**: SHA-256 hash verification for determinism
- **Snapshot Testing**: Golden master regression testing
- **PostgreSQL Fixture**: Testcontainers-based PostgreSQL 16 for integration tests
- **Valkey Fixture**: Redis-compatible caching tests
- **HTTP Fixture**: In-memory API contract testing
- **OpenTelemetry Capture**: Trace and span assertion helpers
- **Test Categories**: Standardized trait constants for CI filtering
---
## Installation
Add `StellaOps.TestKit` as a project reference to your test project:
```xml
```
---
## Quick Start Examples
### 1. Deterministic Time
Eliminate flaky tests caused by time-dependent logic:
```csharp
using StellaOps.TestKit.Deterministic;
using Xunit;
[Fact]
public void Test_ExpirationLogic()
{
// Arrange: Fix time at a known UTC timestamp
using var time = new DeterministicTime(new DateTime(2026, 1, 15, 10, 30, 0, DateTimeKind.Utc));
var expiresAt = time.UtcNow.AddHours(24);
// Act: Advance time to just before expiration
time.Advance(TimeSpan.FromHours(23));
Assert.False(time.UtcNow > expiresAt);
// Advance past expiration
time.Advance(TimeSpan.FromHours(2));
Assert.True(time.UtcNow > expiresAt);
}
```
**API Reference:**
- `DeterministicTime(DateTime initialUtc)` - Create with fixed start time
- `UtcNow` - Get current deterministic time
- `Advance(TimeSpan duration)` - Move time forward
- `SetTo(DateTime newUtc)` - Jump to specific time
---
### 2. Deterministic Random
Reproducible random sequences for property tests and fuzzing:
```csharp
using StellaOps.TestKit.Deterministic;
[Fact]
public void Test_RandomIdGeneration()
{
// Arrange: Same seed produces same sequence
var random1 = new DeterministicRandom(seed: 42);
var random2 = new DeterministicRandom(seed: 42);
// Act
var guid1 = random1.NextGuid();
var guid2 = random2.NextGuid();
// Assert: Reproducible GUIDs
Assert.Equal(guid1, guid2);
}
[Fact]
public void Test_Shuffling()
{
var random = new DeterministicRandom(seed: 100);
var array = new[] { 1, 2, 3, 4, 5 };
random.Shuffle(array);
// Deterministic shuffle order
Assert.NotEqual(new[] { 1, 2, 3, 4, 5 }, array);
}
```
**API Reference:**
- `DeterministicRandom(int seed)` - Create with seed
- `NextGuid()` - Generate deterministic GUID
- `NextString(int length)` - Generate alphanumeric string
- `NextInt(int min, int max)` - Generate integer in range
- `Shuffle(T[] array)` - Fisher-Yates shuffle
---
### 3. Canonical JSON Assertions
Verify JSON determinism for SBOM, VEX, and attestation outputs:
```csharp
using StellaOps.TestKit.Assertions;
[Fact]
public void Test_SbomDeterminism()
{
var sbom = new
{
SpdxVersion = "SPDX-3.0.1",
Name = "MySbom",
Packages = new[] { new { Name = "Pkg1", Version = "1.0" } }
};
// Verify deterministic serialization
CanonicalJsonAssert.IsDeterministic(sbom, iterations: 100);
// Verify expected hash (golden master)
var expectedHash = "abc123..."; // Precomputed SHA-256
CanonicalJsonAssert.HasExpectedHash(sbom, expectedHash);
}
[Fact]
public void Test_JsonPropertyExists()
{
var vex = new
{
Document = new { Id = "VEX-2026-001" },
Statements = new[] { new { Vulnerability = "CVE-2026-1234" } }
};
// Deep property verification
CanonicalJsonAssert.ContainsProperty(vex, "Document.Id", "VEX-2026-001");
CanonicalJsonAssert.ContainsProperty(vex, "Statements[0].Vulnerability", "CVE-2026-1234");
}
```
**API Reference:**
- `IsDeterministic(T value, int iterations)` - Verify N serializations match
- `HasExpectedHash(T value, string expectedSha256Hex)` - Verify SHA-256 hash
- `ComputeCanonicalHash(T value)` - Compute hash for golden master
- `AreCanonicallyEqual(T expected, T actual)` - Compare canonical JSON
- `ContainsProperty(T value, string propertyPath, object expectedValue)` - Deep search
---
### 4. Snapshot Testing
Golden master regression testing for complex outputs:
```csharp
using StellaOps.TestKit.Assertions;
[Fact, Trait("Category", TestCategories.Snapshot)]
public void Test_SbomGeneration()
{
var sbom = GenerateSbom(); // Your SBOM generation logic
// Snapshot will be stored in Snapshots/TestSbomGeneration.json
SnapshotAssert.MatchesSnapshot(sbom, "TestSbomGeneration");
}
// Update snapshots when intentional changes occur:
// UPDATE_SNAPSHOTS=1 dotnet test
```
**Text and Binary Snapshots:**
```csharp
[Fact]
public void Test_LicenseText()
{
var licenseText = GenerateLicenseNotice();
SnapshotAssert.MatchesTextSnapshot(licenseText, "LicenseNotice");
}
[Fact]
public void Test_SignatureBytes()
{
var signature = SignDocument(document);
SnapshotAssert.MatchesBinarySnapshot(signature, "DocumentSignature");
}
```
**API Reference:**
- `MatchesSnapshot(T value, string snapshotName)` - JSON snapshot
- `MatchesTextSnapshot(string value, string snapshotName)` - Text snapshot
- `MatchesBinarySnapshot(byte[] value, string snapshotName)` - Binary snapshot
- Environment variable: `UPDATE_SNAPSHOTS=1` to update baselines
---
### 5. PostgreSQL Fixture
Testcontainers-based PostgreSQL 16 for integration tests:
```csharp
using StellaOps.TestKit.Fixtures;
using Xunit;
public class DatabaseTests : IClassFixture
{
private readonly PostgresFixture _fixture;
public DatabaseTests(PostgresFixture fixture)
{
_fixture = fixture;
}
[Fact, Trait("Category", TestCategories.Integration)]
public async Task Test_DatabaseOperations()
{
// Use _fixture.ConnectionString to connect
using var connection = new NpgsqlConnection(_fixture.ConnectionString);
await connection.OpenAsync();
// Run migrations
await _fixture.RunMigrationsAsync(connection);
// Test database operations
var result = await connection.QueryAsync("SELECT version()");
Assert.NotEmpty(result);
}
}
```
**API Reference:**
- `PostgresFixture` - xUnit class fixture
- `ConnectionString` - PostgreSQL connection string
- `RunMigrationsAsync(DbConnection)` - Apply migrations
- Requires Docker running locally
---
### 6. Valkey Fixture
Redis-compatible caching for integration tests:
```csharp
using StellaOps.TestKit.Fixtures;
public class CacheTests : IClassFixture
{
private readonly ValkeyFixture _fixture;
[Fact, Trait("Category", TestCategories.Integration)]
public async Task Test_CachingLogic()
{
var connection = await ConnectionMultiplexer.Connect(_fixture.ConnectionString);
var db = connection.GetDatabase();
await db.StringSetAsync("key", "value");
var result = await db.StringGetAsync("key");
Assert.Equal("value", result.ToString());
}
}
```
**API Reference:**
- `ValkeyFixture` - xUnit class fixture
- `ConnectionString` - Redis connection string (host:port)
- `Host`, `Port` - Connection details
- Uses `redis:7-alpine` image (Valkey-compatible)
---
### 7. HTTP Fixture Server
In-memory API contract testing:
```csharp
using StellaOps.TestKit.Fixtures;
public class ApiTests : IClassFixture>
{
private readonly HttpClient _client;
public ApiTests(HttpFixtureServer fixture)
{
_client = fixture.CreateClient();
}
[Fact, Trait("Category", TestCategories.Contract)]
public async Task Test_HealthEndpoint()
{
var response = await _client.GetAsync("/health");
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("healthy", body);
}
}
```
**HTTP Message Handler Stub (Hermetic Tests):**
```csharp
[Fact]
public async Task Test_ExternalApiCall()
{
var handler = new HttpMessageHandlerStub()
.WhenRequest("https://api.example.com/data", HttpStatusCode.OK, "{\"status\":\"ok\"}");
var httpClient = new HttpClient(handler);
var response = await httpClient.GetAsync("https://api.example.com/data");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
```
**API Reference:**
- `HttpFixtureServer` - WebApplicationFactory wrapper
- `CreateClient()` - Get HttpClient for test server
- `HttpMessageHandlerStub` - Stub external HTTP dependencies
- `WhenRequest(url, statusCode, content)` - Configure stub responses
---
### 8. OpenTelemetry Capture
Trace and span assertion helpers:
```csharp
using StellaOps.TestKit.Observability;
[Fact]
public async Task Test_TracingBehavior()
{
using var capture = new OtelCapture();
// Execute code that emits traces
await MyService.DoWorkAsync();
// Assert traces
capture.AssertHasSpan("MyService.DoWork");
capture.AssertHasTag("user_id", "123");
capture.AssertSpanCount(expectedCount: 3);
// Verify parent-child hierarchy
capture.AssertHierarchy("ParentSpan", "ChildSpan");
}
```
**API Reference:**
- `OtelCapture(string? activitySourceName = null)` - Create capture
- `AssertHasSpan(string spanName)` - Verify span exists
- `AssertHasTag(string tagKey, string expectedValue)` - Verify tag
- `AssertSpanCount(int expectedCount)` - Verify span count
- `AssertHierarchy(string parentSpanName, string childSpanName)` - Verify parent-child
- `CapturedActivities` - Get all captured spans
---
### 9. Test Categories
Standardized trait constants for CI lane filtering:
```csharp
using StellaOps.TestKit;
[Fact, Trait("Category", TestCategories.Unit)]
public void FastUnitTest() { }
[Fact, Trait("Category", TestCategories.Integration)]
public async Task SlowIntegrationTest() { }
[Fact, Trait("Category", TestCategories.Live)]
public async Task RequiresExternalServices() { }
```
**CI Lane Filtering:**
```bash
# Run only unit tests (fast, no dependencies)
dotnet test --filter "Category=Unit"
# Run all tests except Live
dotnet test --filter "Category!=Live"
# Run Integration + Contract tests
dotnet test --filter "Category=Integration|Category=Contract"
```
**Available Categories:**
- `Unit` - Fast, in-memory, no external dependencies
- `Property` - FsCheck/generative testing
- `Snapshot` - Golden master regression
- `Integration` - Testcontainers (PostgreSQL, Valkey)
- `Contract` - API/WebService contract tests
- `Security` - Cryptographic validation
- `Performance` - Benchmarking, load tests
- `Live` - Requires external services (disabled in CI by default)
---
## Best Practices
### 1. Always Use TestCategories
Tag every test with the appropriate category:
```csharp
[Fact, Trait("Category", TestCategories.Unit)]
public void MyUnitTest() { }
```
This enables CI lane filtering and improves test discoverability.
### 2. Prefer Deterministic Primitives
Avoid `DateTime.UtcNow`, `Guid.NewGuid()`, `Random` in tests. Use TestKit alternatives:
```csharp
// ❌ Flaky test (time-dependent)
var expiration = DateTime.UtcNow.AddHours(1);
// ✅ Deterministic test
using var time = new DeterministicTime(DateTime.UtcNow);
var expiration = time.UtcNow.AddHours(1);
```
### 3. Use Snapshot Tests for Complex Outputs
For large JSON outputs (SBOM, VEX, attestations), snapshot testing is more maintainable than manual assertions:
```csharp
// ❌ Brittle manual assertions
Assert.Equal("SPDX-3.0.1", sbom.SpdxVersion);
Assert.Equal(42, sbom.Packages.Count);
// ...hundreds of assertions...
// ✅ Snapshot testing
SnapshotAssert.MatchesSnapshot(sbom, "MySbomSnapshot");
```
### 4. Isolate Integration Tests
Use TestCategories to separate fast unit tests from slow integration tests:
```csharp
[Fact, Trait("Category", TestCategories.Unit)]
public void FastTest() { /* no external dependencies */ }
[Fact, Trait("Category", TestCategories.Integration)]
public async Task SlowTest() { /* uses PostgresFixture */ }
```
In CI, run Unit tests first for fast feedback, then Integration tests in parallel.
### 5. Document Snapshot Baselines
When updating snapshots (`UPDATE_SNAPSHOTS=1`), add a commit message explaining why:
```bash
git commit -m "Update SBOM snapshot: added new package metadata fields"
```
This helps reviewers understand intentional vs. accidental changes.
---
## Troubleshooting
### Snapshot Mismatch
**Error:** `Snapshot 'MySbomSnapshot' does not match expected.`
**Solution:**
1. Review diff manually (check `Snapshots/MySbomSnapshot.json`)
2. If change is intentional: `UPDATE_SNAPSHOTS=1 dotnet test`
3. Commit updated snapshot with explanation
### Testcontainers Failure
**Error:** `Docker daemon not running`
**Solution:**
- Ensure Docker Desktop is running
- Verify `docker ps` works in terminal
- Check Testcontainers logs: `TESTCONTAINERS_DEBUG=1 dotnet test`
### Determinism Failure
**Error:** `CanonicalJsonAssert.IsDeterministic failed: byte arrays differ`
**Root Cause:** Non-deterministic data in serialization (e.g., random GUIDs, timestamps)
**Solution:**
- Use `DeterministicTime` and `DeterministicRandom`
- Ensure all data is seeded or mocked
- Check for `DateTime.UtcNow` or `Guid.NewGuid()` calls
---
## Migration Guide (Existing Tests)
### Step 1: Add TestKit Reference
```xml
```
### Step 2: Replace Time-Dependent Code
**Before:**
```csharp
var now = DateTime.UtcNow;
```
**After:**
```csharp
using var time = new DeterministicTime(DateTime.UtcNow);
var now = time.UtcNow;
```
### Step 3: Add Test Categories
```csharp
[Fact] // Old
[Fact, Trait("Category", TestCategories.Unit)] // New
```
### Step 4: Adopt Snapshot Testing (Optional)
For complex JSON assertions, replace manual checks with snapshots:
```csharp
// Old
Assert.Equal(expected.SpdxVersion, actual.SpdxVersion);
// ...
// New
SnapshotAssert.MatchesSnapshot(actual, "TestName");
```
---
## CI Integration
### Example `.gitea/workflows/test.yml`
```yaml
name: Test Suite
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Unit Tests (Fast)
run: dotnet test --filter "Category=Unit" --logger "trx;LogFileName=unit-results.trx"
- name: Upload Results
uses: actions/upload-artifact@v4
with:
name: unit-test-results
path: '**/unit-results.trx'
integration:
runs-on: ubuntu-latest
services:
docker:
image: docker:dind
steps:
- uses: actions/checkout@v4
- name: Integration Tests
run: dotnet test --filter "Category=Integration" --logger "trx;LogFileName=integration-results.trx"
```
---
## Support and Feedback
- **Issues:** Report bugs in sprint tracking files under `docs/implplan/`
- **Questions:** Contact Platform Guild
- **Documentation:** `src/__Libraries/StellaOps.TestKit/README.md`
---
## Changelog
### v1.0 (2025-12-23)
- Initial release: DeterministicTime, DeterministicRandom
- CanonicalJsonAssert, SnapshotAssert
- PostgresFixture, ValkeyFixture, HttpFixtureServer
- OtelCapture for OpenTelemetry traces
- TestCategories for CI lane filtering
- Pilot adoption in Scanner.Core.Tests