StellaOps.TestKit Usage Guide
Audience: Stella Ops developers writing unit, integration, and contract tests Source of truth: src/__Libraries/StellaOps.TestKit/ (verify API signatures against the code; this guide is reconciled against it but the code wins on disagreement) Target framework: net10.0 Test stack: xUnit v3 (xunit.v3.*), AwesomeAssertions, FsCheck (property tests), Testcontainers (PostgreSQL/Valkey), OpenTelemetry, ASP.NET WebApplicationFactory
Note:
StellaOps.TestKit.csprojreferencesxunit.v3.*andAwesomeAssertions(not the classic xUnit v2 or FluentAssertions). Consuming test projects should align with the same stack.
Overview
StellaOps.TestKit provides deterministic testing infrastructure for Stella Ops 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 (
postgres:16-alpine) for integration tests - Valkey Fixture: Redis-compatible caching tests (
redis:7-alpineimage) - HTTP Fixture: In-memory API contract testing (
WebApplicationFactory) - OpenTelemetry Capture: Trace and span assertion helpers
- Observability / Log / Metrics contract assertions: Treat telemetry as an API
- Evidence chain traceability: Link tests to requirements and compliance controls
- Post-incident test generation: Scaffold regression tests from replay manifests
- Connector test bases, storage/cache/query templates, blast-radius, interop, longevity, environment-skew helpers (see Additional Modules)
- Test Categories: Standardized trait constants for CI filtering
Installation
Add StellaOps.TestKit as a project reference to your test project. The library lives at src/__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj; adjust the relative path for your test project’s depth:
<ItemGroup>
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
Quick Start Examples
1. Deterministic Time
Eliminate flaky tests caused by time-dependent logic:
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: (StellaOps.TestKit.Deterministic.DeterministicTime, IDisposable)
DeterministicTime(DateTime startUtc)- Create with fixed start time.startUtc.Kindmust beDateTimeKind.Utc, otherwise anArgumentExceptionis thrown.UtcNow- Get current deterministic time (DateTime)UtcNowOffset- Get current deterministic time asDateTimeOffset(zero offset)Advance(TimeSpan duration)- Move time forward (or backward with a negative span)SetTo(DateTime newUtc)- Jump to a specific time (must be UTC)Reset(DateTime startUtc)- Reset to a starting value (must be UTC)
2. Deterministic Random
Reproducible random sequences for property tests and fuzzing:
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: (StellaOps.TestKit.Deterministic.DeterministicRandom)
DeterministicRandom(int seed)- Create with seedSeed- The seed used by this instanceNext()/Next(int maxValue)/Next(int minValue, int maxValue)- Random integers (there is noNextInt(...)method; use theNext(...)overloads)NextDouble()- Random double in[0.0, 1.0)NextBytes(byte[] buffer)/NextBytes(Span<byte> buffer)- Fill a buffer with random bytesNextGuid()- Generate deterministic GUIDNextString(int length)- Generate alphanumeric stringNextElement<T>(T[] array)- Select a random element (throws on null/empty array)Shuffle<T>(T[] array)- Fisher-Yates shuffle (in place)static CreateRandom(int seed)- Create aSystem.Randomseeded the same way (for interop)
3. Canonical JSON Assertions
Verify JSON determinism for SBOM, VEX, and attestation outputs:
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" }
};
// Object property verification (dot-delimited path).
CanonicalJsonAssert.ContainsProperty(vex, "Document.Id", "VEX-2026-001");
}
Path limitation:
ContainsPropertysplits thepropertyPathon.and walks object properties only (case-insensitively). It does not support array indexing — a path likeStatements[0].Vulnerabilitywill not resolve, becauseStatements[0]is treated as a literal object-property name. To assert into arrays, serialize and inspect the element directly, or compare the whole structure withAreCanonicallyEqual/ a snapshot.
API Reference: (StellaOps.TestKit.Assertions.CanonicalJsonAssert, static; hashing delegates to StellaOps.Canonical.Json.CanonJson)
IsDeterministic<T>(T value, int iterations = 10)- Verify N serializations produce identical bytes (default 10 iterations)HasExpectedHash<T>(T value, string expectedSha256Hex)- Verify SHA-256 hash (comparison is case-insensitive)ComputeCanonicalHash<T>(T value)- Compute the canonical SHA-256 hash (lowercase hex) for a golden masterAreCanonicallyEqual<T>(T expected, T actual)- Compare canonical JSON bytesMatchesJson<T>(T value, string expectedJson)- Assert the canonical JSON string equals the expected literal (debugging aid)ContainsProperty<T>(T value, string propertyPath, object expectedValue)- Object-property deep search (dot path; see limitation above)
4. Snapshot Testing
Golden master regression testing for complex outputs:
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:
[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>(T value, string snapshotName)- JSON snapshotMatchesTextSnapshot(string value, string snapshotName)- Text snapshotMatchesBinarySnapshot(byte[] value, string snapshotName)- Binary snapshot- Environment variable:
UPDATE_SNAPSHOTS=1to update baselines
5. PostgreSQL Fixture
Testcontainers-based PostgreSQL (postgres:16-alpine) for integration tests. PostgresFixture is an IAsyncLifetime fixture (it starts the container in InitializeAsync), usable as an xUnit IClassFixture<> or — to share one container across classes — via the supplied [Collection("Postgres")] collection fixture (PostgresCollection).
using StellaOps.TestKit.Fixtures;
using Npgsql;
using Xunit;
public class DatabaseTests : IClassFixture<PostgresFixture>
{
private readonly PostgresFixture _fixture;
public DatabaseTests(PostgresFixture fixture)
{
_fixture = fixture;
// Optional: register migration scripts to apply per isolated session.
// _fixture.RegisterMigrations("MyModule", "Migrations/001_init.sql");
}
[Fact, Trait("Category", TestCategories.Integration)]
public async Task Test_DatabaseOperations()
{
// Get an isolated session (default isolation = SchemaPerTest).
// Registered migrations are applied to the new schema automatically.
await using var session = await _fixture.CreateSessionAsync(nameof(Test_DatabaseOperations));
await using var connection = new NpgsqlConnection(session.ConnectionString);
await connection.OpenAsync();
await using var cmd = new NpgsqlCommand("SELECT version()", connection);
var version = (string?)await cmd.ExecuteScalarAsync();
Assert.NotNull(version);
}
}
Migration helpers (there is no RunMigrationsAsync(DbConnection) method):
RegisterMigrations(string module, string scriptPath)- Register a SQL file applied to each isolated sessionApplyMigrationsAsync(string schemaName)- Apply registered file-based migrations to a schema (rewritespublic.to the target schema)ApplyMigrationsFromAssemblyAsync(Assembly assembly, string schemaName, string? resourcePrefix = null)andApplyMigrationsFromAssemblyAsync<TAssemblyMarker>(string schemaName, string? resourcePrefix = null)- Apply embedded-resource*.sqlmigrations
API Reference: (StellaOps.TestKit.Fixtures.PostgresFixture, IAsyncLifetime)
ConnectionString- Connection string for the container’stestdbdatabaseDatabaseName("testdb"),Host,Port- Connection detailsIsolationMode-PostgresIsolationMode(SchemaPerTest(default),Truncation,DatabasePerTest)CreateSessionAsync(string? testName = null)- Create an isolatedPostgresTestSession(ConnectionString,Schema; disposing drops the per-test schema/database)CreateSchemaSessionAsync(...)/CreateDatabaseSessionAsync(...)- Force a specific isolation strategyExecuteSqlAsync(string sql),TruncateAllTablesAsync(),CreateDatabaseAsync,DropDatabaseAsync,DropSchemaAsync,GetConnectionString(string databaseName)PostgresCollection/[CollectionDefinition("Postgres")]- Share the container across test classes- Requires Docker running locally
6. Valkey Fixture
Redis-compatible caching for integration tests. ValkeyFixture is an IAsyncLifetime fixture. The preferred entry point is CreateSessionAsync, which returns an isolated ValkeyTestSession exposing a ready IDatabase:
using StellaOps.TestKit.Fixtures;
public class CacheTests : IClassFixture<ValkeyFixture>
{
private readonly ValkeyFixture _fixture;
public CacheTests(ValkeyFixture fixture) => _fixture = fixture;
[Fact, Trait("Category", TestCategories.Integration)]
public async Task Test_CachingLogic()
{
await using var session = await _fixture.CreateSessionAsync(nameof(Test_CachingLogic));
await session.Database.StringSetAsync("key", "value");
var result = await session.Database.StringGetAsync("key");
Assert.Equal("value", result.ToString());
}
}
You can also connect directly with _fixture.ConnectionString / _fixture.GetDatabase(index) if you manage isolation yourself.
API Reference: (StellaOps.TestKit.Fixtures.ValkeyFixture, IAsyncLifetime)
ConnectionString- Connection string (formathost:port)Host,Port,Connection(ConnectionMultiplexer?) - Connection detailsIsolationMode-ValkeyIsolationMode(DatabasePerTest(default),FlushDb,FlushAll)CreateSessionAsync(string? testName = null)- IsolatedValkeyTestSession(Connection,Database,DatabaseIndex; disposing flushes the session DB)GetDatabase(int dbIndex = 0),FlushDatabaseAsync(int index),FlushAllAsync()- Uses the
redis:7-alpineimage (Valkey is Redis wire-compatible). The code comment notes you can substitute avalkey/valkeyimage in production deployments, but the fixture itself pullsredis:7-alpine.
7. HTTP Fixture Server
In-memory API contract testing:
using StellaOps.TestKit.Fixtures;
public class ApiTests : IClassFixture<HttpFixtureServer<Program>>
{
private readonly HttpClient _client;
public ApiTests(HttpFixtureServer<Program> 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):
[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: (StellaOps.TestKit.Fixtures)
HttpFixtureServer<TProgram>- subclassesWebApplicationFactory<TProgram>; hosts the app under the"Test"environmentHttpFixtureServer(Action<IServiceCollection>? configureServices = null)- Optionally replace dependencies with test doublesCreateClient()/CreateClient(Action<HttpClient> configure)- Get anHttpClientfor the in-memory serverHttpMessageHandlerStub- Stub external HTTP dependencies (aHttpMessageHandler); unmatched requests return404unless a default is setWhenRequest(string url, HttpStatusCode statusCode, string? content = null)- Configure a simple stubbed responseWhenRequest(string url, Func<HttpRequestMessage, Task<HttpResponseMessage>> handler)- Configure a custom responseWhenAnyRequest(Func<HttpRequestMessage, Task<HttpResponseMessage>> handler)- Default handler for unmatched URLs
- Related:
WebServiceFixture<TProgram>(alsoIAsyncLifetime) for richer ASP.NET hosting with deterministic test services andCreateAuthenticatedClient(...)
8. OpenTelemetry Capture
Trace and span assertion helpers:
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: (StellaOps.TestKit.Observability.OtelCapture, IDisposable)
OtelCapture(string? activitySourceName = null)- Create capture (null = capture all activity sources)AssertHasSpan(string spanName)- Verify span exists (matchesDisplayNameorOperationName)AssertHasTag(string tagKey, string expectedValue)- Verify at least one span carries the tagAssertSpanHasTag(string spanName, string tagKey, string expectedValue)- Verify a specific span’s tagAssertSpanCount(int expectedCount)- Verify span countAssertHierarchy(string parentSpanName, string childSpanName)- Verify parent-child relationshipCapturedActivities- Get all captured spans (IReadOnlyList<Activity>)Clear()- Discard captured activities
9. Observability Contract Testing
Contract assertions for treating logs, metrics, and traces as APIs:
OTel Contract Testing:
using StellaOps.TestKit.Observability;
[Fact, Trait("Category", TestCategories.Contract)]
public async Task Test_SpanContracts()
{
using var capture = new OtelCapture("MyService");
await service.ProcessRequestAsync();
// Verify required spans are present
OTelContractAssert.HasRequiredSpans(capture, "ProcessRequest", "ValidateInput", "SaveResult");
// Verify span attributes
var span = capture.CapturedActivities.First();
OTelContractAssert.SpanHasAttributes(span, "user_id", "tenant_id", "correlation_id");
// Check attribute cardinality (prevent metric explosion)
OTelContractAssert.AttributeCardinality(capture, "http_method", maxCardinality: 10);
// Detect high-cardinality attributes globally
OTelContractAssert.NoHighCardinalityAttributes(capture, threshold: 100);
}
Log Contract Testing:
using StellaOps.TestKit.Observability;
using System.Text.RegularExpressions;
[Fact]
public async Task Test_LogContracts()
{
var logCapture = new List<CapturedLogRecord>();
// ... capture logs during test execution ...
// Verify required fields
LogContractAssert.HasRequiredFields(logCapture[0], "CorrelationId", "TenantId");
// Ensure no PII leakage
var piiPatterns = new[]
{
new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"), // Email
new Regex(@"\b\d{3}-\d{2}-\d{4}\b"), // SSN
};
LogContractAssert.NoSensitiveData(logCapture, piiPatterns);
// Verify log level appropriateness
LogContractAssert.LogLevelAppropriate(logCapture[0], LogLevel.Information, LogLevel.Warning);
// Ensure error logs have correlation for troubleshooting
LogContractAssert.ErrorLogsHaveCorrelation(logCapture, "CorrelationId", "RequestId");
}
Metrics Contract Testing:
using StellaOps.TestKit.Observability;
[Fact]
public async Task Test_MetricsContracts()
{
using var capture = new MetricsCapture("MyService");
await service.ProcessMultipleRequests();
// Verify required metrics exist
MetricsContractAssert.HasRequiredMetrics(capture, "requests_total", "request_duration_seconds");
// Check label cardinality bounds
MetricsContractAssert.LabelCardinalityBounded(capture, "http_requests_total", maxLabels: 50);
// Verify counter monotonicity
MetricsContractAssert.CounterMonotonic(capture, "processed_items_total");
// Verify gauge bounds
MetricsContractAssert.GaugeInBounds(capture, "active_connections", minValue: 0, maxValue: 1000);
}
API Reference: (StellaOps.TestKit.Observability; all contract violations throw ContractViolationException)
OTelContractAssert (static):
HasRequiredSpans(OtelCapture capture, params string[] spanNames)- Verify spans existSpanHasAttributes(Activity span, params string[] attributeNames)- Verify a span’s attributesAttributeCardinality(OtelCapture capture, string attributeName, int maxCardinality)- Bound one attribute’s cardinalityNoHighCardinalityAttributes(OtelCapture capture, int threshold = 100)- Detect cardinality explosionSpanNamesMatchPattern(OtelCapture capture, string pattern)- Enforce span-name naming conventionAllSpansHaveStatus(OtelCapture capture)- Require every span to set a status (notUnset)ErrorSpansHaveAttributes(OtelCapture capture, params string[] requiredErrorAttributes)- Require attributes on error spansNoSensitiveDataInSpans(OtelCapture capture, params Regex[] sensitivePatterns)- Detect PII in span attributes
LogContractAssert (static; operates over IEnumerable<CapturedLogRecord> / a single CapturedLogRecord):
HasRequiredFields(CapturedLogRecord record, params string[] fieldNames)- Verify scope/state fields presentNoSensitiveData(IEnumerable<CapturedLogRecord> records, IEnumerable<Regex> piiPatterns)- Check for PII in messages, state, exceptionsLogLevelAppropriate(CapturedLogRecord record, LogLevel minLevel, LogLevel maxLevel)- Bound the log levelErrorLogsHaveCorrelation(IEnumerable<CapturedLogRecord> records, params string[] correlationFields)- Require correlation fields onError+ logsMessagesMatchPattern(IEnumerable<CapturedLogRecord> records, Regex formatPattern)- Enforce message formatNoLogsAboveLevel(IEnumerable<CapturedLogRecord> records, LogLevel maxAllowedLevel)- Fail if logs exceed a levelCapturedLogRecord(record) - fields:LogLevel,Message,EventId,Exception,ScopeValues,StateValues,Timestamp,Category. (The TestKit does not ship aLogCapturecollector — populate aList<CapturedLogRecord>from your own logger provider.)
MetricsContractAssert (static):
MetricExists(MetricsCapture capture, string metricName)/HasRequiredMetrics(capture, params string[] metricNames)LabelCardinalityBounded(capture, string metricName, int maxLabels)- Bound label cardinalityCounterMonotonic(capture, string metricName)- Verify a counter never decreasesGaugeInBounds(capture, string metricName, double minValue, double maxValue)- Bound gauge valuesMetricNamesMatchPattern(capture, string pattern)- Enforce metric-name conventionNoUnboundedLabels(capture, params Regex[] forbiddenLabelPatterns)- Detect unbounded label valuesMetricsCapture(IDisposable) - Capture metrics during test execution; exposesMetricNames,HasMetric,GetValues,GetLabelCardinality,GetLabelsContractViolationException- Thrown when contracts are violated
10. Evidence Chain Traceability
Link tests to requirements for regulatory compliance and audit trails:
Requirement Attribute:
using StellaOps.TestKit.Evidence;
[Fact]
[Requirement("REQ-AUTH-001", SprintTaskId = "AUTH-0127-001")]
public async Task Test_UserAuthentication()
{
// Verify authentication works as required
}
[Fact]
[Requirement("REQ-AUDIT-002", SprintTaskId = "AUDIT-0127-003", ComplianceControl = "SOC2-AU-12")]
public void Test_AuditLogImmutability()
{
// Verify audit logs cannot be modified
}
Filtering tests by requirement:
# Run tests for a specific requirement
dotnet test --filter "Requirement=REQ-AUTH-001"
# Run tests for a sprint task
dotnet test --filter "SprintTask=AUTH-0127-001"
# Run tests for a compliance control
dotnet test --filter "ComplianceControl=SOC2-AU-12"
Evidence Chain Assertions:
using StellaOps.TestKit.Evidence;
[Fact]
[Requirement("REQ-EVIDENCE-001")]
public void Test_ArtifactHashStability()
{
var artifact = GenerateEvidence(input);
// Verify artifact produces expected hash (golden master)
EvidenceChainAssert.ArtifactHashStable(artifact, "abc123...expected-sha256...");
}
[Fact]
[Requirement("REQ-DETERMINISM-001")]
public void Test_EvidenceImmutability()
{
// Verify generator produces identical output across iterations
EvidenceChainAssert.ArtifactImmutable(() => GenerateEvidence(fixedInput), iterations: 100);
}
[Fact]
[Requirement("REQ-TRACE-001")]
public void Test_TraceabilityComplete()
{
var requirementId = "REQ-EVIDENCE-001";
var testId = "MyTests.TestMethod";
var artifactHash = EvidenceChainAssert.ComputeSha256(artifact);
// Verify all traceability components present
EvidenceChainAssert.TraceabilityComplete(requirementId, testId, artifactHash);
}
Traceability Report Generation:
using StellaOps.TestKit.Evidence;
// Generate traceability matrix from test assemblies
var reporter = new EvidenceChainReporter();
reporter.AddAssembly(typeof(MyTests).Assembly);
var report = reporter.GenerateReport();
// Output as Markdown
Console.WriteLine(report.ToMarkdown());
// Output as JSON
Console.WriteLine(report.ToJson());
API Reference: (StellaOps.TestKit.Evidence)
RequirementAttribute(string requirementId)- Link a test (or class) to a requirement; emits anxUnitRequirementtrait (implementsXunit.v3.ITraitAttribute)RequirementAttribute.SprintTaskId- Link to sprint task (optional; emits aSprintTasktrait)RequirementAttribute.ComplianceControl- Link to compliance control (optional; emits aComplianceControltrait)RequirementAttribute.SourceDocument- Path/URL to the source requirement document (optional; not emitted as a trait but surfaced in the traceability report)EvidenceChainAssert.ArtifactHashStable(byte[] artifact, string expectedHashHex)/ArtifactHashStable(string content, string expectedHashHex)- Verify SHA-256 hash (case-insensitive)EvidenceChainAssert.ArtifactImmutable(Func<byte[]> generator, int iterations = 10)/ArtifactImmutable(Func<string> generator, int iterations = 10)- Verify deterministic output (iterationsmust be ≥ 2)EvidenceChainAssert.ComputeSha256(byte[] data)/ComputeSha256(string content)- Compute lowercase-hex SHA-256EvidenceChainAssert.RequirementLinked(string requirementId)/RequirementLinked(MethodInfo testMethod)- Assert requirement linkageEvidenceChainAssert.TraceabilityComplete(string requirementId, string testId, string artifactId)- Verify all chain components presentEvidenceChainReporter.AddAssembly(Assembly assembly)- Add an assembly to scan for[Requirement]EvidenceChainReporter.GenerateReport()- Generate anEvidenceChainReportEvidenceChainReport.ToMarkdown()- Markdown outputEvidenceChainReport.ToJson()- JSON outputEvidenceTraceabilityException- Thrown when evidence assertions fail
11. Test Categories
Standardized trait constants for CI lane filtering:
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:
# 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 (constants on StellaOps.TestKit.TestCategories; values equal their name, e.g. TestCategories.Unit == "Unit"):
Core lanes:
Unit- Fast, in-memory, no external dependenciesProperty- FsCheck/generative testingSnapshot- Golden master regressionIntegration- Testcontainers (PostgreSQL, Valkey)Contract- API/WebService contract testsSecurity- Cryptographic validationPerformance- Benchmarking, load testsLive- Requires external services (disabled in CI by default)
Pipeline-aligned lanes:
Architecture,Golden,Benchmark,AirGap,E2E,Chaos,Determinism,Resilience,Observability
Storage-specific:
StorageConcurrency,StorageIdempotency,QueryDeterminism,StorageMigration
Schema evolution:
SchemaEvolution,ConfigDiff
Distributed-systems / advanced:
HLC,Federation,Latency,Immutability,Parity
Testing-enhancements lanes:
PostIncident- Tests derived from production incidents (P1/P2 block releases)EvidenceChain- Requirement traceability testsLongevity- Time-extended stability tests (run nightly, not PR-gating)Interop- Cross-version compatibility tests (release-gating)EnvironmentSkew- Tests across varied infrastructure profiles
Blast-radius annotations (nested class TestCategories.BlastRadius, used as a separate BlastRadius trait, not the Category trait):
Auth,Scanning,Evidence,Compliance,Advisories,RiskPolicy,Crypto,Integrations,Persistence,Api
[Fact]
[Trait("Category", TestCategories.Integration)]
[Trait("BlastRadius", TestCategories.BlastRadius.Auth)]
public async Task TestTokenValidation() { }
// Filter: dotnet test --filter "BlastRadius=Auth|BlastRadius=Api"
12. Post-Incident Testing
Generate regression tests from production incidents:
Generate Test Scaffold from Incident:
using StellaOps.TestKit.Incident;
// Create incident metadata
var metadata = new IncidentMetadata
{
IncidentId = "INC-2026-001",
OccurredAt = DateTimeOffset.Parse("2026-01-15T10:30:00Z"),
RootCause = "Race condition in concurrent bundle creation",
AffectedModules = ["EvidenceLocker", "Policy"],
Severity = IncidentSeverity.P1,
Title = "Evidence bundle duplication"
};
// Generate test scaffold from replay manifest
var generator = new IncidentTestGenerator();
var scaffold = generator.GenerateFromManifestJson(manifestJson, metadata);
// Output generated test code
var code = scaffold.GenerateTestCode();
File.WriteAllText($"Tests/{scaffold.TestClassName}.cs", code);
Generated Test Structure:
[Trait("Category", TestCategories.PostIncident)]
[Trait("Incident", "INC-2026-001")]
[Trait("Severity", "P1")]
public sealed class Incident_INC_2026_001_Tests
{
[Fact]
public async Task Validates_RaceCondition_Fix()
{
// Arrange - fixtures from replay manifest
// Act - execute the incident scenario
// Assert - verify fix prevents recurrence
}
}
Filter Post-Incident Tests:
# Run all post-incident tests
dotnet test --filter "Category=PostIncident"
# Run only P1/P2 tests (release-gating)
dotnet test --filter "Category=PostIncident&(Severity=P1|Severity=P2)"
# Run tests for a specific incident
dotnet test --filter "Incident=INC-2026-001"
API Reference: (StellaOps.TestKit.Incident)
IncidentMetadata(record) - Incident context:IncidentId,OccurredAt,RootCause,AffectedModules,Severity(required); plus optionalTitle,ReportUrl,ResolvedAt,CorrelationIds,FixTaskId,TagsIncidentSeverity-P1(critical) throughP4(low impact)IncidentTestGenerator.GenerateFromManifestJson(string manifestJson, IncidentMetadata metadata)- Generate scaffoldIncidentTestGenerator.RegisterIncidentTest(string incidentId, TestScaffold scaffold)/RegisteredTests- Track generated scaffoldsIncidentTestGenerator.GenerateReport()-IncidentTestReportsummary of registered incident testsTestScaffold.GenerateTestCode()- Output C# test code (usesAwesomeAssertions+Xunitusings)TestScaffold.ToJson()/static FromJson(string json)- Serialize/deserialize scaffold
See Post-Incident Testing Guide for complete documentation.
Additional Modules
The TestKit ships several further helpers beyond the sections above. These are part of StellaOps.TestKit today (verify signatures in source before relying on them); they are summarized here for completeness rather than fully documented:
- Connector test bases (
StellaOps.TestKit.Connectors):ConnectorParserTestBase<,>,ConnectorFetchTestBase<,>,ConnectorResilienceTestBase,ConnectorSecurityTestBase,ConnectorLiveSchemaTestBase,ConnectorHttpFixture, andFixtureUpdaterfor offline connector fixtures and drift detection.LiveTestAttribute/LiveTheoryAttributegate tests that require live endpoints. - Storage / cache / query templates (
StellaOps.TestKit.Templates): abstractIClassFixture-based bases —StorageIdempotencyTests<TEntity,TKey>,StorageConcurrencyTests<TEntity,TKey>,QueryDeterminismTests<TEntity,TKey>(onPostgresFixture),CacheIdempotencyTests<TEntity,TKey>(onValkeyFixture),WebServiceContractTestBase<TProgram>/WebServiceNegativeTestBase<TProgram>/WebServiceAuthTestBase<TProgram>/WebServiceOtelTestBase<TProgram>, and theFlakyToDeterministicPatternhelper. - Web service fixture (
StellaOps.TestKit.Fixtures):WebServiceFixture<TProgram>andContractTestHelper(withSchemaBreakingChanges) for ASP.NET contract testing. - Environment skew (
StellaOps.TestKit.Environment):EnvironmentProfile/CpuProfile/NetworkProfile/ResourceLimitsandSkewTestRunner(throwsSkewAssertException) to run a test across varied infrastructure profiles. Pairs with theEnvironmentSkewcategory. - Interop / schema versioning (
StellaOps.TestKit.Interop):VersionCompatibilityFixture,SchemaVersionMatrix, and supporting result/report types for N-1/N+1 compatibility. Pairs with theInteropcategory. - Longevity (
StellaOps.TestKit.Longevity):StabilityTestRunner+StabilityTestConfigandStabilityMetrics/StabilityReportfor time-extended stability runs. Pairs with theLongevitycategory. - Blast radius (
StellaOps.TestKit.BlastRadius):BlastRadiusValidator,BlastRadiusTestRunner, and report/violation records that drive incident-targeted test runs against theBlastRadiustraits. - Test intents & coverage (
StellaOps.TestKit.Traits/.Analysis):IntentAttribute(ITraitAttribute) +TestIntentsconstants for tagging why a test exists (e.g. regulatory), andIntentCoverageReportGenerator/IntentCoverageReportfor intent-coverage reporting.
Best Practices
1. Always Use TestCategories
Tag every test with the appropriate category:
[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:
// ❌ 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:
// ❌ 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:
[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:
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:
- Review diff manually (check
Snapshots/MySbomSnapshot.json) - If change is intentional:
UPDATE_SNAPSHOTS=1 dotnet test - Commit updated snapshot with explanation
Testcontainers Failure
Error: Docker daemon not running
Solution:
- Ensure Docker Desktop is running
- Verify
docker psworks 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
DeterministicTimeandDeterministicRandom - Ensure all data is seeded or mocked
- Check for
DateTime.UtcNoworGuid.NewGuid()calls
Migration Guide (Existing Tests)
Step 1: Add TestKit Reference
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
Step 2: Replace Time-Dependent Code
Before:
var now = DateTime.UtcNow;
After:
using var time = new DeterministicTime(DateTime.UtcNow);
var now = time.UtcNow;
Step 3: Add Test Categories
[Fact] // Old
[Fact, Trait("Category", TestCategories.Unit)] // New
Step 4: Adopt Snapshot Testing (Optional)
For complex JSON assertions, replace manual checks with snapshots:
// Old
Assert.Equal(expected.SpdxVersion, actual.SpdxVersion);
// ...
// New
SnapshotAssert.MatchesSnapshot(actual, "TestName");
CI Integration
The CI lanes live under .gitea/workflows/ (e.g. dotnet-pr-tests.yml, test-manifest-execution.yml, test-architecture.yml, dotnet-nightly-sweep.yml). The repo does not ship a single test.yml; the snippet below is an illustrative example of how to filter by TestCategories in a workflow — adapt it to the existing lanes rather than adding a new file.
Example workflow (illustrative)
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
This changelog records the early release history only. The current TestKit surface is substantially larger (observability/log/metrics contract assertions, evidence-chain traceability, post-incident generation, connector test bases, storage/cache/query templates, interop, longevity, environment-skew, blast-radius, and test-intent helpers). Treat
src/__Libraries/StellaOps.TestKit/as the authoritative inventory.
Initial release
- DeterministicTime, DeterministicRandom
- CanonicalJsonAssert, SnapshotAssert
- PostgresFixture, ValkeyFixture, HttpFixtureServer
- OtelCapture for OpenTelemetry traces
- TestCategories for CI lane filtering
