Onboarding guide
Testing Architecture & Solution Composition
Chapter scope note. The tier chapters (
tier-00through the sweep) document every type in the production codebase one by one. Test types are the logged exception: this chapter covers the ~784 types that live in test projects, grouped by project purpose and foundational infrastructure, not written as one section per[Fact]. Individual test methods are cited only as worked examples. Cross-reference the tier chapters for the production types being tested.
This chapter teaches the complete testing architecture of the MMCA workspace: how the solutions are sliced, which CI filter sees which tests, how Microsoft Testing Platform differs from VSTest, the shipped testing-infrastructure NuGet packages, the NetArchTest fitness-function suites that act as executable governance, the integration and E2E strategies, and three worked examples that tie the pieces together.
1. Solution composition and the test runner
Solution files: slnx vs. slnf
Both repos use the same two-file pattern:
| File | Purpose |
|---|---|
MMCA.Common.slnx |
Full human solution, all source + most test projects |
MMCA.ADC.slnx |
Full human solution, all source + all in-scope test projects |
MMCA.ADC.CI.slnf |
CI fast path, source + unit/arch/WebAPI tests only (excludes Integration, E2E, AppHost, MAUI) |
MMCA.ADC.Integration.slnf |
SQL-gated integration tests only |
MMCA.Store.CI.slnf |
Store CI fast path, mirrors the ADC pattern |
MMCA.ADC.CI.slnf (MMCA.ADC/MMCA.ADC.CI.slnf) includes every source project and every
unit/architecture/WebAPI test project, but deliberately excludes:
Tests/Integration/MMCA.ADC.Identity.IntegrationTestsTests/Integration/MMCA.ADC.Conference.IntegrationTestsTests/Integration/MMCA.ADC.Engagement.IntegrationTestsTests/E2E/MMCA.ADC.E2E.Tests- The Aspire
AppHostand MAUI UI projects
Why: the integration tests need a real SQL Server (the ADC_TEST_SQL_BASE connection string points
to a CI SQL service container), and the E2E tests need the full Aspire stack running. Neither is
available in the fast build job, so they are deliberately gated behind a second CI job
(integration-tests) that provisions SQL. That job gates the deploy job, the full integration
suite (~290 tests across the three per-service projects) must pass before any code ships.
[Rubric §17, DevOps & Deployment]: §17 assesses how consistently CI/CD enforces quality gates;
the two-filter pattern is how the build stays fast on every push while the SQL-dependent gate still
blocks deployment.
MMCA.ADC.Integration.slnf (MMCA.ADC/MMCA.ADC.Integration.slnf) contains exactly three
projects (MMCA.ADC.Identity.IntegrationTests, MMCA.ADC.Conference.IntegrationTests,
MMCA.ADC.Engagement.IntegrationTests), these are the per-service WebApplicationFactory
integration tests that replaced the now-retired combined MMCA.ADC.IntegrationTests project.
The old project (Tests/Integration/MMCA.ADC.IntegrationTests) is still on disk but is excluded
from MMCA.ADC.slnx because it referenced the deleted MMCA.ADC.WebAPI host; its 34 types are
being re-homed onto the per-service projects.
MMCA.Common.slnx (MMCA.Common/MMCA.Common.slnx) includes all thirteen source packages, the four
Core (.Shared, .Domain, .Application, .Infrastructure), three Presentation (.API, .Grpc,
.UI), two Aspire (.Aspire, .Aspire.Hosting), and four Testing (.Testing, .Testing.Architecture,
.Testing.E2E, .Testing.UI), plus nine test projects. Two projects are intentionally absent from
the .slnx:
Tests/Presentation/MMCA.Common.UI.Gallery, a backend-less Blazor host that renders the realLoginPage,RegisterPage, and a UI-primitives showcase; it exists solely to give Playwright something to hit.Tests/Presentation/MMCA.Common.UI.E2E.Tests, the axe-core + render-smoke suite that hits the Gallery.
Both are excluded so dotnet test --solution MMCA.Common.slnx stays fast (no browser, no
network). They run in the dedicated ui-e2e CI job, built by csproj path:
dotnet test --project Tests/Presentation/MMCA.Common.UI.E2E.Tests/MMCA.Common.UI.E2E.Tests.csproj.
[Rubric §28, Front-End Testing & Quality]: §28 assesses whether UI components have automated
tests; the Gallery + E2E split is the mechanism that adds browser-level coverage without slowing
the primary test loop.
Microsoft Testing Platform (MTP), not VSTest
All three repos share one global.json structure:
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
(MMCA.Common/global.json:1-5, identical in MMCA.ADC/global.json and MMCA.Store/global.json)
This selects MTP as the test runner instead of the legacy VSTest runner. The practical consequences:
Exit code 8, if a test project discovers zero tests MTP exits 8, not 0. Every CI
dotnet testcall includes--minimum-expected-tests 1to surface this as a visible failure rather than a silent skip (seeMMCA.Common/CLAUDE.md).Filter syntax differs. You pass a
--separator and then MTP's own filter flags:# Run a single test class dotnet test --project Tests/Modules/Identity/MMCA.ADC.Identity.Domain.Tests \ -- -class "*UserTests*" # Run a single test method dotnet test --project Tests/Modules/Identity/MMCA.ADC.Identity.Domain.Tests \ -- -method "*Create_WithValidData_ReturnsSuccess*"The flags are
-classand-method, not the VSTest--filter FullyQualifiedName~...form. Always target--project <csproj>, never a bare directory path.Running compiled test binaries directly. Because the headless CI environment has no reachable SQL, integration tests build but cannot be run-verified via
dotnet test. The workaround is to run the compiled.Tests.exedirectly for unit tests:./bin/Release/net10.0/MMCA.Common.Shared.Tests.exe, the MTP binary is self-contained. [Rubric §33, Developer Experience & Inner Loop]: §33 assesses the friction of the local development cycle; MTP's self-hosted test binaries and the explicit--minimum-expected-testsguard both reduce "tests silently went away" surprises.
2. Test project layout
The inventory below is drawn from 00-inventory.md (test-assembly counts) and the solution files
above. Counts are distinct types per project as reported by the Roslyn inventory scan.
MMCA.Common, 391 test types across 9 in-solution projects + 2 out-of-solution
Unit, Core layer
| Project | Types | Purpose |
|---|---|---|
MMCA.Common.Shared.Tests |
18 | Unit tests for the Result pattern, Error, ErrorType, value objects, DTO contracts |
MMCA.Common.Domain.Tests |
40 | Unit tests for entity hierarchy, aggregate root, domain events, specifications, soft-delete |
MMCA.Common.Application.Tests |
130 | Unit tests for CQRS dispatcher, decorator pipeline, module loader, IMessageBus, validators |
MMCA.Common.Infrastructure.Tests |
133 | Unit/integration tests for EF base contexts, outbox processor, repository, caching, JWT generation, JWKS provider, data-source resolver |
Unit, Presentation layer
| Project | Types | Purpose |
|---|---|---|
MMCA.Common.API.Tests |
33 | Tests for ApiControllerBase, exception handlers, idempotency filter, middleware, JWKS endpoint |
MMCA.Common.Grpc.Tests |
3 | Tests for GrpcResultExceptionInterceptor, JwtForwardingClientInterceptor, Result↔RpcException mapping |
MMCA.Common.UI.Tests |
13 | bUnit component tests for shared Blazor components (login/register forms, nav, theming) |
Hosting
| Project | Types | Purpose |
|---|---|---|
MMCA.Common.Aspire.Tests |
4 | Tests for AddServiceDefaults, health-check registration, OutboxPollFilterProcessor telemetry suppression |
Architecture
| Project | Types | Purpose |
|---|---|---|
MMCA.Common.Architecture.Tests |
8 | NetArchTest layer dependency, domain purity, microservice extraction, PII/IAnonymizable, aggregate factory, MassTransit v8 pin |
Out-of-solution (UI/E2E, run via dedicated CI job)
| Project | Types | Purpose |
|---|---|---|
MMCA.Common.UI.Gallery |
6 | Backend-less Blazor host; renders real Login/Register pages + primitives showcase for Playwright to hit |
MMCA.Common.UI.E2E.Tests |
7 | Playwright axe-core accessibility scans + render smoke against the Gallery; asserts WCAG 2.1 AA |
MMCA.ADC, 393 test types across 22 in-solution projects (E2E in slnx but not slnf)
Unit, per-module, per-layer (Identity module)
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.Identity.Domain.Tests |
4 | User aggregate factory methods, invariants, soft-delete |
MMCA.ADC.Identity.Application.Tests |
14 | Command/query handler tests for register, login, profile management |
MMCA.ADC.Identity.Shared.Tests |
3 | DTO/enum tests |
MMCA.ADC.Identity.API.Tests |
6 | Controller helper tests, rate-limit bypass |
MMCA.ADC.Identity.Infrastructure.Tests |
4 | Token service, JWKS provider, EF configuration |
MMCA.ADC.Identity.UI.Tests |
5 | bUnit tests for Profile, login route authorization |
Unit, per-module, per-layer (Conference module)
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.Conference.Domain.Tests |
14 | Event/Session/Speaker aggregate factory, invariants, domain events |
MMCA.ADC.Conference.Application.Tests |
120 | Handler tests for all 14 Conference controllers' use cases (bulk) |
MMCA.ADC.Conference.Shared.Tests |
13 | DTO validation, enum coverage |
MMCA.ADC.Conference.API.Tests |
13 | Controller registration, route tests |
MMCA.ADC.Conference.Infrastructure.Tests |
5 | EF configuration, repository behavior |
MMCA.ADC.Conference.UI.Tests |
7 | bUnit tests for session/speaker card components |
Unit, per-module, per-layer (Engagement module)
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.Engagement.Domain.Tests |
2 | Bookmark aggregate |
MMCA.ADC.Engagement.Application.Tests |
8 | Bookmark command/query handlers |
MMCA.ADC.Engagement.Shared.Tests |
2 | DTO tests |
MMCA.ADC.Engagement.API.Tests |
2 | Controller surface |
MMCA.ADC.Engagement.Infrastructure.Tests |
2 | EF config |
Architecture
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.Architecture.Tests |
15 | NetArchTest layer dependency, module isolation, domain purity, microservice extraction/transport, PII/IAnonymizable, naming conventions, entity conventions, handler conventions, concurrency, DTO placement, controller conventions, integration-event contracts |
Integration (per-service WebApplicationFactory, in MMCA.ADC.Integration.slnf only)
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.Identity.IntegrationTests |
14 | Full HTTP tests of the Identity service host against real SQL Server; auth flows, attendee/organizer access, outbox fidelity |
MMCA.ADC.Conference.IntegrationTests |
25 | Full HTTP tests of the Conference service host |
MMCA.ADC.Engagement.IntegrationTests |
7 | Full HTTP tests of the Engagement service host |
WebAPI middleware
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.WebAPI.Tests |
1 | Host-neutral unit tests for MMCA.Common.API exception handlers |
E2E (in MMCA.ADC.slnx but excluded from both .slnf filters)
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.E2E.Tests |
49 | Playwright browser-automation tests across login, register, conference browsing, bookmark flows; requires Aspire stack running |
Out-of-scope (not in slnx)
| Project | Types | Purpose |
|---|---|---|
MMCA.ADC.IntegrationTests |
34 | Legacy combined integration tests (referenced deleted MMCA.ADC.WebAPI host); excluded, being re-homed |
MMCA.Common test-type totals
Summing the 9 in-solution projects: 18 + 40 + 130 + 133 + 33 + 3 + 13 + 4 + 8 = 382 plus the 2 out-of-solution projects adding 6 + 7 = 395 total (inventory shows 391 in-solution, minor rounding from the scan).
MMCA.ADC test-type totals
Summing the in-scope ADC projects: 4+14+3+6+4+5 + 14+120+13+13+5+7 + 2+8+2+2+2 + 15 + 14+25+7 +
1 + 49 = 334 (inventory shows 393 counting the out-of-slnx MMCA.ADC.IntegrationTests).
Combined test-type count reconciliation:
The inventory lists 784 total test/testing types when all test projects and the shipped
infrastructure packages (the four testing packages, MMCA.Common.Testing, .Testing.E2E,
.Testing.UI, .Testing.Architecture; e.g. MMCA.Common.Testing = 5, MMCA.Common.Testing.E2E = 15)
are summed. The breakdown: ~391 MMCA.Common test/testing types + ~393 MMCA.ADC test types = 784.
[Rubric §14, Testability & Test Strategy]: §14 assesses the breadth and meaningfulness of the test suite across all layers; the project layout above, unit per layer, arch per repo, integration per service, E2E for browser flows, demonstrates deliberate stratification rather than a single catch-all integration tier.
3. Shipped testing-infrastructure packages
MMCA.Common ships four testing-infrastructure packages that downstream apps consume as NuGet references rather than writing their own harness infrastructure:
MMCA.Common.Testing, integration-test base, JWT generator, fixtures, entity builders (this section).MMCA.Common.Testing.E2E, Playwright fixtures, Blazor nav helpers, Identity page objects, axe-core enforcement (this section).MMCA.Common.Testing.UI, bUnit component-test base + MudBlazor provider harness (used by the per-module*.UI.Testsprojects; see the bUnit worked example in §6).MMCA.Common.Testing.Architecture, the shared NetArchTest fitness-function rule library + abstract test bases (the 13th framework package; covered in §4, where each repo's*.Architecture.Testsconsumes it).
MMCA.Common.Testing
MMCA.Common/Source/Hosting/MMCA.Common.Testing/, 5 types, shipped as MMCA.Common.Testing.
IIntegrationTestFixture
MMCA.Common/Source/Hosting/MMCA.Common.Testing/IIntegrationTestFixture.cs:1
The shared contract between a WebApplicationFactory fixture and IntegrationTestBase<TFixture>.
Two members: CreateClient() returns an HttpClient configured for the test server, and
ResetDatabaseAsync() resets the database between tests. The doc comment (IIntegrationTestFixture.cs:16-20)
explicitly notes that fixtures for database-per-service hosts must reset every relational source
by enumerating IEntityDataSourceRegistry and IDataSourceResolver, this is the
database-per-microservice (ADR-006) implication for test cleanup. Each concrete fixture implements
this by opening a per-source SqlConnection and calling Respawner.ResetAsync.
IntegrationTestBase<TFixture>
MMCA.Common/Source/Hosting/MMCA.Common.Testing/IntegrationTestBase.cs:13
The abstract base class all integration test classes inherit from. Generic on TFixture : IIntegrationTestFixture. Implements xUnit v3's IAsyncLifetime:
InitializeAsync()callsFixture.ResetDatabaseAsync()before each test, so tests always start from a clean database state (line 31).DisposeAsync()disposes the testHttpClientafter each test (line 33-38).SetBearerToken(string token)writes aBearerAuthorizationheader ontoClient(line 43-44).ClearAuthentication()removes it (line 47-48).GetAsync<T>,PostAsync<T>,PutAsync<T>,PutAsync(no body),DeleteAsync, typed HTTP helpers (lines 51-75).NextId(), thread-safeInterlocked.Incrementover a static counter starting at 1000, giving tests unique integer IDs without collisions under parallel execution (line 75).
Downstream projects extend this by adding role helpers. For example,
IdentityIntegrationTestBase (MMCA.ADC/Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestBase.cs:13)
adds AuthenticateAsOrganizer(userId), AuthenticateAsAttendee(userId), and
AuthenticateAsSpeaker(userId, speakerId), each calls SetBearerToken(JwtTokenGenerator.GenerateToken(...)).
JwtTokenGenerator
MMCA.Common/Source/Hosting/MMCA.Common.Testing/JwtTokenGenerator.cs:29
A static class that mints RS256 JWT tokens for test consumption. Key design decisions visible in the source:
- RS256 matches production. The generator uses
RSA.Create()+ImportFromPem+RsaSecurityKey(lines 122-129), matching the algorithm configured byAddCommonAuthenticationin the real host. Integration tests therefore exercise the actual JWKS/RS256 validation path, not a relaxed HMAC shortcut. [Rubric §11, Security]: §11 assesses how well the test suite validates the security model; using the same algorithm in tests as in production is a direct embodiment. - Committed keypair, documented as insecure.
DefaultPublicKeyPemandDefaultPrivateKeyPem(lines 48-95) are embedded in the source and committed to the public repo. The class doc (lines 21-27) explicitly warns: "⚠ Security note: the embedded RSA keypair is committed to the public git repo and is therefore insecure by design, it exists solely to make integration tests deterministic without a per-run key-generation step. Never configure a production deployment with this keypair." DefaultKeyId = "mmca-test-key"(line 40), thekidclaim that test host appsettings expose viaJwks:KeyId, soRsaJwksProviderpublishes a JWKS entry with the samekidand the middleware's key resolution succeeds.GenerateToken(audience, userId, role, additionalClaims?, privateKeyPem?, issuer?, keyId?)(line 111) exports the RSA parameters before disposal (line 124), createsRsaSecurityKey, and writes thesub/user_id/roleclaims to match the shapeITokenServiceproduces in production (lines 131-137).
EntityBuilderBase<TBuilder, TEntity>
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9
A fluent-builder generic base for test entity construction. Subclasses configure sensible defaults
(so tests only set the property under test) and implement Build(), which calls the domain factory
method and throws on a Failure result. This keeps test arrange code readable without exposing
EF's parameterless constructor. [Rubric §14, Testability]: builder patterns are a classic
indicator of testability investment; the shared base means every downstream module gets fluent
builders "for free" by subclassing.
FeatureManagementTestExtensions
MMCA.Common/Source/Hosting/MMCA.Common.Testing/FeatureManagementTestExtensions.cs:12
A static extension on IServiceCollection (ConfigureTestFeatureFlags(Dictionary<string, bool>),
line 21) that injects an in-memory IConfiguration and registers AddFeatureManagement against
it. Call this in a WebApplicationFactory.ConfigureServices override to override feature flags
from appsettings.json. Allows integration tests to exercise both the flag-on and flag-off code
paths without changing the real config file.
MMCA.Common.Testing.E2E
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/, 15 types, shipped as
MMCA.Common.Testing.E2E. [Rubric §28, Front-End Testing & Quality]: §28 assesses whether UI
components have browser-level automated coverage; this package is the shared foundation for that
coverage.
E2ETestConfiguration
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:8
All E2E configuration is environment-variable driven. Properties:
| Variable | Default | Effect |
|---|---|---|
E2E_BASE_URL |
https://localhost:7108 |
Target app URL (overridden per downstream project) |
E2E_HEADLESS |
true (absent = headless) |
Set false to watch tests visually |
E2E_TIMEOUT |
30_000 ms |
Per-action Playwright timeout |
E2E_SLOWMO |
0 ms |
Delay between Playwright actions (for visual debugging) |
E2E_BROWSER |
chromium |
Engine: chromium, firefox, or webkit |
E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD |
admin@localhost / Admin123! |
Seeded admin credentials |
E2E_CUSTOMER_EMAIL / E2E_CUSTOMER_PASSWORD |
user@localhost / User123! |
Seeded user credentials |
Downstream projects supply app-specific defaults via [ModuleInitializer] on the Default*
setters. Environment variables always take precedence. [Rubric §22, Responsive & Cross-Browser]:
E2E_BROWSER is the mechanism for running the same suite against all three browser engines in CI.
PlaywrightFixture + E2ETestCollection
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6
PlaywrightFixture is an xUnit v3 IAsyncLifetime collection fixture. InitializeAsync() (line 12)
creates a IPlaywright instance, then resolves the browser type from E2ETestConfiguration.Browser
via a switch on the upper-cased value (lines 17-22): "FIREFOX" → Playwright.Firefox,
"WEBKIT" → Playwright.Webkit, any other value → Playwright.Chromium. The comment on line 21
calls out the rubric §22 cross-browser intent explicitly. DisposeAsync (line 26) disposes browser
and playwright in order. E2ETestCollection is the xUnit [CollectionDefinition] that wires the
fixture to all classes decorated with [Collection(E2ETestCollection.Name)].
E2ETestBase
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:7
The abstract base all E2E test classes inherit from. Decorates itself [Collection(E2ETestCollection.Name)]
so it receives the PlaywrightFixture singleton. InitializeAsync() (line 18) creates a fresh
IBrowserContext (with IgnoreHTTPSErrors: true for local dev TLS) and a new IPage per test,
setting DefaultTimeout from config. DisposeAsync() (line 27) closes the page and disposes
the context, guaranteeing test isolation at the browser-session level.
Key methods:
LoginAsync(email, password)(line 43), navigates to/login, fills email and password viaFillFieldAsync(guarded against Blazor re-hydration), clicks the "Sign in" button, then races the logout button against an error alert usingTask.WhenAny(line 72). If the error alert wins, throwsInvalidOperationExceptionwith the alert text, tests get a meaningful failure message rather than a timeout.LoginAsAdminAsync()/LoginAsUserAsync(), delegate toLoginAsyncwith credentials fromE2ETestConfiguration.RegisterNewUserAsync(firstName?, lastName?)(line 86), synthesizes a unique email withGuid.NewGuid().ToString("N")[..8], fills the registration form, submits, and races the same logout-button / error-alert pair for completion.FillFieldAsync(ILocator, string)(line 136), delegates toPageExtensions.FillAndVerifyAsync, the shared Blazor re-hydration guard (see below).UniqueId()(line 139), eight-char GUID fragment for unique test data.
PageExtensions
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:13
Extension methods on IPage and ILocator that isolate Blazor InteractiveAuto rendering quirks:
WaitForBlazorAsync(page, timeout)(line 19), pollswindow.Blazor?._internaluntil truthy (the WASM runtime is ready), then evaluates tworequestAnimationFrame+ 500ms to let the render pipeline flush. Without this, event handlers are not attached and clicks/fills are silently ignored.GotoAndWaitForBlazorAsync(page, path)(line 37), combinesGotoAsync+WaitForLoadStateAsync(Load)(notNetworkIdle, Blazor's SignalR WebSocket keeps a persistent connection open, soNetworkIdleis never reached) +WaitForBlazorAsync.BlazorNavigateAsync(page, path)(line 49), usesBlazor.navigateTofor client-side SPA navigation (avoids a full page reload), then waits forwindow.location.pathnameto change and the render cycle to flush. Used for auth-protected pages when already logged in.WaitForPageAndBlazorAsync(page)(line 71), waits forLoadstate + render flush; use after link/button clicks that trigger full-page navigation.FillAndVerifyAsync(field, value, timeout)(line 91), fills a form field then assertsToHaveValueAsyncwith Playwright's built-in retry. If the pre-render value was wiped by re-hydration (a common Blazor InteractiveAuto timing bug), it falls back toPressSequentiallyAsyncwith 20ms key delay and re-asserts. This is the single shared fill helper for the whole E2E layer.AssertNoAccessibilityViolationsAsync(page, options?)(line 118), runspage.RunAxe()fromDeque.AxeCore.Playwright, collects violations, and throwsAccessibilityViolationExceptionif any are found. The exception message includes impact, rule ID, help text, and node count per violation. [Rubric §21, Accessibility]: §21 assesses whether the app meets WCAG 2.1 AA; this extension method is the single enforcement point.
AccessibilityViolationException
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs
A typed exception thrown by AssertNoAccessibilityViolationsAsync. Giving axe-core violations
their own exception type means test runners display the violation summary in the failure message
without requiring the test author to parse raw JSON.
Page objects: LoginPage, RegisterPage, ProfilePage
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/PageObjects/
Pre-built page-object models for the three Identity pages that every downstream app shares.
LoginPage and RegisterPage encapsulate form locators and the fill/submit sequence; they use
FillAndVerifyAsync internally. ProfilePage exposes profile-field locators and the "Change
Password" / "Delete Account" button locators. Downstream projects extend by composition or
inheritance.
Workflow base classes: UserLoginTestsBase, UserRegistrationTestsBase, ProfileManagementTestsBase, LogoutTestsBase
MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Workflows/Identity/
Abstract test bases for the four standard Identity workflows. Each extends E2ETestBase, provides
concrete [Fact] methods that apply to any app using MMCA.Common's Identity UI, and calls
AssertNoAccessibilityViolationsAsync after the page is interactive. A downstream app's E2E suite
inherits from these bases and optionally overrides or adds app-specific assertions.
[Rubric §21, Accessibility]: axe-core is called at the workflow level, not just on the page
object, so new flows inherit the a11y check without extra work.
4. Architecture fitness tests, executable governance
[Rubric §34, Architecture Governance & Documentation]: §34 assesses whether architectural decisions are documented, enforced, and kept honest over time; fitness functions are the "enforced" axis. [Rubric §3, Clean Architecture]: §3 assesses whether the layering is real (code can't reference upward) or aspirational (rules stated in a README but never enforced). The architecture tests make the rules real.
The primer explains (00-primer.md#architecture-enforcement-is-doubled-fitness-functions) that
layer rules are enforced twice: at compile-time (the MSBuild .targets file) and at runtime
(NetArchTest). This section covers the runtime half.
NetArchTest.eNhancedEdition, the mechanism
NetArchTest works on compiled assemblies, not source. It loads an Assembly and applies
Types.InAssembly(assembly).ShouldNot().HaveDependencyOnAny(namespacePrefix).GetResult().
GetResult() returns a TestResult with a IsSuccessful flag and a list of failing type names.
The shared ArchitectureAssert.NoViolations(result, reason) method converts a failure
into an xUnit assertion failure whose message includes the reason string and the offending type
names, so a developer adding a forbidden reference sees exactly which type broke the rule and why.
The rule bodies live once, in a shipped package. The 13th framework package,
MMCA.Common.Testing.Architecture, holds the reusable rule library (ArchitectureRules.* partial
classes, Layers, Purity, Transport, Modules, Handlers, Entities, Naming, etc.) and a set
of abstract *TestsBase classes (one per rule family, in Bases/), all parameterized by an
IArchitectureMap. Each repo's *.Architecture.Tests project consumes the package and supplies its own
map: CommonArchitectureMap (MMCA.Common, every layer is a framework layer, one anchor type per package)
and AdcArchitectureMap (MMCA.ADC, adds the per-module Identity/Conference/Engagement layers). A
concrete test class is then a ~10-line sealed subclass, e.g.:
public sealed class LayerDependencyTests : LayerDependencyTestsBase
{
protected override IArchitectureMap Map { get; } = new CommonArchitectureMap();
}
(MMCA.Common.Architecture.Tests/LayerDependencyTests.cs:9-12). The map's anchor types
(CommonArchitectureMap.cs:15-21, e.g. typeof(Common.Domain.Entities.BaseEntity<>).Assembly) mean the
assembly path is always the one actually compiled into the test run, not a hard-coded path. Because both
repos drive the same rule bodies, the architecture rules stay identical across all three repos and a
fix to a rule propagates with the next package bump rather than needing a hand-copy.
The walkthroughs below describe what each rule enforces (and the count of facts/theories it produces);
the rule implementations now live in the shared package's ArchitectureRules.* + *TestsBase files,
not in the per-repo test class.
MMCA.Common.Architecture.Tests, 8 types
Located at MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/. The 8 are thin subclasses:
LayerDependencyTests, DomainPurityTests, MicroserviceExtractionTests, PiiConventionTests,
AggregateConventionTests, DependencyVersionTests, EventVersioningConventionTests, and
FrameworkSanityTests (the last holds the Common-only checks the shared library does not generalize).
LayerDependencyTests
MMCA.Common.Architecture.Tests/LayerDependencyTests.cs
13 [Fact] methods, one per forbidden directed edge in the layer graph:
Shared_ShouldNotDependOn_Domain/Application/Infrastructure/Api(4 facts)Domain_ShouldNotDependOn_Application/Infrastructure/Api(3 facts)Application_ShouldNotDependOn_Infrastructure/Api(2 facts)Infrastructure_ShouldNotDependOn_Api(1 fact)UI_ShouldNotDependOn_Application/Infrastructure/Domain(3 facts)
The last three confirm the two deliberate exceptions to the standard stack (UI and Grpc depend
only on Shared) are honored in the other direction, UI must not depend on Application,
Infrastructure, or Domain.
DomainPurityTests
MMCA.Common.Architecture.Tests/DomainPurityTests.cs
4 facts asserting that Domain and Shared contain no references to:
Microsoft.AspNetCore, Microsoft.EntityFrameworkCore, Serilog, AutoMapper,
Newtonsoft.Json, FluentValidation, Scrutor, MudBlazor, Polly, Stripe,
StackExchange.Redis (the forbidden-dependency list now lives in the shared
ArchitectureRules.Purity partial in MMCA.Common.Testing.Architecture).
Application additionally must not depend on Microsoft.EntityFrameworkCore or Microsoft.AspNetCore.
[Rubric §3, Clean Architecture]: this is the runtime enforcement that the domain is genuinely
framework-independent, not just convention.
MicroserviceExtractionTests
MMCA.Common.Architecture.Tests/MicroserviceExtractionTests.cs
Enforces the transport-isolation invariant for microservice extraction (ADR-007/ADR-008):
Application, Domain, and Shared must not reference MassTransit; transport (gRPC / MassTransit)
belongs only in Infrastructure and the transport-edge packages. The rule body is the shared
MicroserviceExtractionTestsBase over CommonArchitectureMap.
The Common-only sanity checks that the shared library does not generalize, Grpc must not depend on
Domain/Application/Infrastructure, IMessageBus lives in Application, and IJwksProvider lives
in Infrastructure, now live in FrameworkSanityTests
(MMCA.Common.Architecture.Tests/FrameworkSanityTests.cs:20-43), not in MicroserviceExtractionTests.
These pin the abstractions and the transport package to their correct layer so a future refactor doesn't
quietly move them.
PiiConventionTests
MMCA.Common.Architecture.Tests/PiiConventionTests.cs
1 fact: any type in MMCA.Common.Domain bearing a [Pii]-decorated property must implement
IAnonymizable (ADR-005). The test passes vacuously today (the framework ships no PII-bearing data
subject entity), and fails the build the moment one is added without an erasure path.
[Rubric §30, Compliance, Privacy & Data Governance]: §30 assesses whether GDPR/CCPA erasure
obligations are structurally enforced; this test is the structural gate.
AggregateConventionTests
MMCA.Common.Architecture.Tests/AggregateConventionTests.cs
2 facts checking DDD factory-method conventions via reflection (NetArchTest cannot inspect method
return types, so the file uses Assembly.GetTypes() directly):
Domain_ShouldExpose_AggregateRoots, asserts the filter finds at least one aggregate root (guards against the test becoming vacuous).AggregateRoots_ShouldHave_StaticCreateFactory_ReturningResultOfTheAggregate, walks every non-abstract type inheritingAuditableAggregateRootEntity<>, asserts it has a public staticCreate(...)method whose return type isResult<TAggregate>. [Rubric §4, Domain-Driven Design]: §4 assesses whether the domain model is authentic DDD; the factory-method +Result<T>return is the DDD "factory prevents invalid entity construction" pattern, here enforced automatically.
DependencyVersionTests
MMCA.Common.Architecture.Tests/DependencyVersionTests.cs
3 [Theory] rows (one per MassTransit package: MassTransit, MassTransit.RabbitMQ,
MassTransit.Azure.ServiceBus.Core): parse Directory.Packages.props by walking up from
AppContext.BaseDirectory until the file is found, extract the <PackageVersion Include="MassTransit" Version="..."/> attribute, and assert version.Major < 9. The class is a bare subclass of the shared
DependencyVersionTestsBase (which holds the parsing + theory rows); MMCA.Common is the only repo that
declares it, because it is the one that actually pins MassTransit. The failure message explains exactly
why: "MassTransit v9 requires a commercial license (MT_LICENSE); without it every broker-enabled
host fails the startup license check and crashes." [Rubric §32, Dependency & Supply-Chain]: §32
assesses whether dependency versions are tracked, pinned, and protected against accidental bumps;
DependencyVersionTests is the build-time gate for the most dangerous version upgrade in the
codebase. (See the primer 00-primer.md#nuget-lock-files--pinned-audited-sources for context.)
MMCA.ADC.Architecture.Tests, 15 types
Located at MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/. Like the Common project, these
are thin subclasses of the shared *TestsBase classes from MMCA.Common.Testing.Architecture
(referenced as a NuGet package, MMCA.ADC.Architecture.Tests.csproj:25), each supplying AdcArchitectureMap,
the map that adds the per-module Identity/Conference/Engagement layers on top of the Common framework
layers. The rule bodies are shared; only the map differs from the Common repo.
LayerDependencyTests (ADC)
MMCA.ADC.Architecture.Tests/LayerDependencyTests.cs
Extends the Common checks to the ADC module assemblies. Uses [Theory, InlineData("Identity"), InlineData("Conference"), InlineData("Engagement")] to parameterize, a single fact body run
three times. Checks all forbidden pairs: module Domain must not depend on its own Application,
Infrastructure, or API; module Application must not depend on its own Infrastructure or API;
module Infrastructure must not depend on its own API. Additionally checks that MMCA.Common.Domain
and MMCA.Common.Application (via ModuleAssemblies.CommonDomain/CommonApplication) hold the
same inward rules when consumed in ADC.
ModuleIsolationTests
MMCA.ADC.Architecture.Tests/ModuleIsolationTests.cs
The cross-module boundary rules, the modular monolith's core governance. Each parameterized
theory for the three modules: a module's Domain must not depend on any other module's Domain;
a module's Application must not depend on any other module's Application; Infrastructure must not
cross into other Infrastructure; API must not cross into other API. Two additional rules: a
module's Domain must not touch other Infrastructure, and a module's Application must not touch
other Infrastructure. Cross-module communication is allowed only through *.Shared (contract)
layers and DI-injected interfaces. The "all other modules' namespaces at this layer" list is generated
dynamically by the shared ModuleIsolationTestsBase / ArchitectureRules.Modules from the modules
declared in AdcArchitectureMap.
MicroserviceExtractionTests (ADC)
MMCA.ADC.Architecture.Tests/MicroserviceExtractionTests.cs
The transport-isolation rule applied across all ADC module layers. The shared rule's transport
namespace set covers MassTransit, Grpc, and Google.Protobuf. For Domain, Application, and Shared
of all three modules, plus the Common Domain/Application layers, none of these transport namespaces
may appear. Transport belongs only in Infrastructure, the *.Service hosts, and *.Contracts projects.
A NetArchTest quirk worth knowing: it matches by namespace prefix, so 'Grpc' catches
Grpc.Core / Grpc.Net.* / Grpc.AspNetCore but NOT the project's own MMCA.Common.Grpc (which starts
with MMCA), so the rule does not accidentally flag the framework's own transport package.
PiiConventionTests (ADC)
MMCA.ADC.Architecture.Tests/PiiConventionTests.cs
The ADC variant scans all module domain assemblies (ModuleAssemblies.AllModuleDomainAssemblies,
an array of Identity + Conference + Engagement domains). It first asserts the list is non-empty
(the test must not become vacuous), then checks every [Pii]-decorated entity implements
IAnonymizable. The doc comment notes the deliberate scope: the Identity User aggregate is
PII-bearing; the Conference Speaker is not, because speaker names and emails are public agenda
content sourced from Sessionize, not app-user PII subject to the privacy policy's erasure right.
Additional ADC-only test classes
| Class | File | Focus |
|---|---|---|
DomainPurityTests |
DomainPurityTests.cs |
Framework-independence of all module Domain layers |
HandlerConventionTests |
HandlerConventionTests.cs |
All command/query handlers properly implement the right interface |
ConcurrencyConventionTests |
ConcurrencyConventionTests.cs |
Async handler methods accept CancellationToken as final parameter |
ControllerConventionTests |
ControllerConventionTests.cs |
Controllers inherit ApiControllerBase, declare [ApiVersion] |
EntityConventionTests |
EntityConventionTests.cs |
Entities inherit the correct base class |
NamingConventionTests |
NamingConventionTests.cs |
Handler/repo/service naming follows conventions |
EventConventionTests |
EventConventionTests.cs |
Domain events follow naming/shape conventions |
ImmutabilityTests |
ImmutabilityTests.cs |
DTOs/events/value objects are immutable (records, no public setters) |
SharedLayerTests |
SharedLayerTests.cs |
Shared projects contain only DTOs, enums, identifier aliases |
IntegrationEventContractTests |
IntegrationEventContractTests.cs |
Integration events implement the correct interface, are records, and are in the correct namespace |
DataResidencyTests |
DataResidencyTests.cs |
Compliance drift guard (§30): the data-residency region in PRIVACY.md must match the SQL region in deploy.yml, does not use AdcArchitectureMap (reads the files directly) |
5. Integration and E2E strategy
Per-service integration tests (SQL-gated)
The three integration test projects (MMCA.ADC.Identity.IntegrationTests,
MMCA.ADC.Conference.IntegrationTests, MMCA.ADC.Engagement.IntegrationTests) each boot their
service in-process with WebApplicationFactory<Program>. The concrete fixture for each service
(e.g. IdentityIntegrationTestFixture,
MMCA.ADC/Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs)
implements IIntegrationTestFixture and handles the full lifecycle:
- Database creation. A throwaway SQL Server database is created with a GUID-embedded name
(
ADC_IdentityIntegrationTest_{Guid:N}) on the server given byADC_TEST_SQL_BASE. If the env var is absent, it defaults to(localdb)\MSSQLLocalDB(line 26-27). - Environment injection. Before the host builds, connection strings, JWT keypair, JWKS
settings, and rate-limit thresholds are injected via
Environment.SetEnvironmentVariable(lines 102-117). Using process environment variables (rather thanConfigureAppConfiguration) is necessary because the host reads these at configure-time beforeWebApplicationFactory'sConfigureServicesoverride runs. The saved original values are restored inRestoreEnvironmentafter the fixture disposes. - Schema migration. Creating the first
HttpClientforcesProgram.csto run, which callsInitializeDatabaseAsync(DatabaseInitStrategy.Migrate), the migrations are applied to the fresh database automatically (line 58-59). - Per-test reset via Respawn.
Respawner.CreateAsync(line 63) configures Respawn to delete all rows except__EFMigrationsHistory. Each test callsResetDatabaseAsync()throughIntegrationTestBase.InitializeAsync()before its body runs. This is faster than dropping/re-creating the database and handles foreign key cascades automatically. - Disposal. On fixture disposal,
SqlConnection.ClearAllPools()releases connection pool handles (line 143), then a rawDROP DATABASEcommand removes the throwaway database.
Cross-service gRPC edges are faked for isolation: the non-Identity services re-point their
AddForwardedJwtBearer scheme at the in-process test key, and gRPC clients that would call other
services use stub implementations. This keeps each integration suite testing only its own service's
behavior. [Rubric §14, Testability]: §14 awards credit for isolation, per-test DB reset plus
per-service WebApplicationFactory means each test is truly independent.
MMCA.Common unit-level infrastructure tests
MMCA.Common.Infrastructure.Tests (133 types) uses SQLite-backed EnsureCreated contexts for
tests that need a real EF pipeline. SQLite avoids the SQL Server dependency entirely, which is why
MMCA.Common builds and tests without any SQL Server or Docker in the local environment. The tradeoff
is that SQLite doesn't support all SQL Server features (e.g., row-level locking, certain index
hints), but EF Core's cross-provider abstraction is sufficient for the behaviors being tested
(outbox persistence, repository queries, soft-delete filters).
E2E tests
MMCA.ADC.E2E.Tests (49 types) and MMCA.Common.UI.E2E.Tests (7 types) require either the
full Aspire stack (dotnet run --project Source/Hosting/MMCA.ADC.AppHost) or the Gallery
backend (MMCA.Common.UI.Gallery) respectively. The Aspire AppHost starts SQL Server, Redis,
RabbitMQ, MailDev, all four service hosts, the Gateway, and the UI, it cannot be launched
headlessly in a background bash shell (it stalls at the control-plane init). The CI workaround
documented in the codebase is a pre-warmed Aspire environment with a timeout guard.
The cross-browser matrix runs the same suite three times by varying E2E_BROWSER:
chromium → firefox → webkit. Mobile is covered by responsive layout testing (grid→card at
narrow viewport), not a separate app. [Rubric §22, Responsive & Cross-Browser/Device]: §22
assesses whether the browser support matrix is exercised; the E2E_BROWSER env var is the
mechanism. [Rubric §21, Accessibility]: the AssertNoAccessibilityViolationsAsync call in
workflow bases and in MMCA.Common.UI.E2E.Tests is the runtime axe-core gate for WCAG 2.1 AA.
6. Worked examples
Three examples tie the infrastructure above to real test code.
Example A, Architecture fitness function (DependencyVersionTests)
The per-repo class is a bare subclass; the [Theory] body lives once in the shared base:
// MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DependencyVersionTests.cs:9-11
public sealed class DependencyVersionTests : DependencyVersionTestsBase { }
// the rows + parsing live in the shipped package:
// MMCA.Common.Testing.Architecture/Bases/DependencyVersionTestsBase.cs
[Theory]
[InlineData("MassTransit")]
[InlineData("MassTransit.RabbitMQ")]
[InlineData("MassTransit.Azure.ServiceBus.Core")]
public void MassTransit_MustNotExceedMajorVersion8(string packageId)
{
var version = ReadPinnedVersion(packageId); // parse Directory.Packages.props
version.Should().NotBeNull(because: $"...");
version!.Major.Should().BeLessThan(9,
because: "MassTransit v9 requires a commercial license ...");
}
Why this test exists and what it protects. MassTransit v9 introduced a mandatory commercial
license check (MT_LICENSE). A broker-enabled service that starts without it crashes at startup,
but the crash happens at runtime, not at compile time, and CI never starts the broker. So a
dotnet outdated --upgrade that silently bumped MassTransit from 8.5.5 to 9.x would compile,
pass all tests, deploy, and then crash in production. DependencyVersionTests makes this a
build-time failure: the shared base parses Directory.Packages.props directly, not the compiled
assembly, so the failure happens in the build-and-test job before any code ships. This is the
pattern of a fitness function: an executable rule that enforces a policy that cannot be expressed
as a type error. [Rubric §32, Dependency & Supply-Chain]: this is the highest-signal embodiment
of §32 in the codebase, a deliberate, documented pin with an automated gate.
Example B, Integration test base usage (IdentityIntegrationTestBase)
// MMCA.ADC/Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestBase.cs:11
[Collection(IdentityIntegrationTestCollection.Name)]
public abstract class IdentityIntegrationTestBase(IdentityIntegrationTestFixture fixture)
: IntegrationTestBase<IdentityIntegrationTestFixture>(fixture)
{
private const string Audience = "AtlDevConapi";
protected void AuthenticateAsOrganizer(UserIdentifierType userId = 1)
=> SetBearerToken(JwtTokenGenerator.GenerateToken(Audience, userId, "Organizer"));
// ...
}
A concrete test (AttendeeAuthTests, AttendeeProfileTests, etc.) inherits
IdentityIntegrationTestBase. The lifecycle is:
IdentityIntegrationTestFixturestarts once per collection (xUnit class fixture), migrates the database, and creates theRespawner.IntegrationTestBase.InitializeAsync()callsFixture.ResetDatabaseAsync()before each test.- The test calls
AuthenticateAsAttendee(userId)which callsSetBearerToken(...), subsequentClient.GetAsync(...)/PostAsync(...)calls carry theBearerheader. IntegrationTestBase.DisposeAsync()disposesClient.- After all tests run,
IdentityIntegrationTestFixture.DisposeAsync()drops the throwaway database.
The test author writes only the business assertion. All boilerplate (token minting, HTTP client, DB reset, teardown) is in the shared infrastructure. This is exactly why the shared package exists. [Rubric §14, Testability]: the fixture isolation + shared infrastructure is a direct measure of the §14 "integration test isolation" criterion.
Example C, bUnit component test (ProfileTests)
// MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Profile/ProfileTests.cs:32
public sealed class ProfileTests : BunitTestBase
{
public ProfileTests()
{
Services.AddSingleton(Mock.Of<IUserUIService>());
Services.AddSingleton(Mock.Of<IAuthUIService>());
}
[Fact]
public void WhenAuthenticatedWithUserId_RendersProfileActions()
{
var cut = RenderAs<ProfilePage>(UserWithId, _ => { });
cut.WaitForAssertion(() =>
{
cut.Markup.Should().Contain("Change Password");
cut.Markup.Should().Contain("Delete My Account");
});
}
[Fact]
public void WhenUserIdClaimMissing_RendersErrorState()
{
var cut = RenderAs<ProfilePage>(UserWithoutId, _ => { });
cut.WaitForAssertion(() => cut.Markup.Should().Contain("Unable to load profile."));
}
}
BunitTestBase (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/BunitTestBase.cs:12)
registers MudBlazor.Services, sets JSInterop.Mode = JSRuntimeMode.Loose (so MudBlazor's JS
calls don't throw), registers AddAuthorizationCore, and exposes a mutable
AuthenticationStateProvider (the MutableAuthenticationStateProvider inner class, line 52)
that the ProfilePage injects directly. RenderAs<TComponent>(principal, parameters) (line 39)
sets the provider's principal, then renders the component with a cascading AuthenticationState
wrapping the same principal. RenderUnderTest<TComponent> (line 34) is a shortcut for the
anonymous case.
The test constructs UserWithId (a principal with a user_id=42 claim) and UserWithoutId
(a principal without it), then asserts the two render states. cut.WaitForAssertion is bUnit's
polling assertion, it retries until the assertion passes or a timeout elapses, handling
Blazor's async rendering cycle.
[Rubric §28, Front-End Testing & Quality]: bUnit tests the component's render logic in isolation without a browser, giving fast feedback on UI states that are awkward to exercise in Playwright. The combination of bUnit (fast, component-level) + Playwright (slow, browser-level) is the two-tier UI test strategy: bUnit for render-state logic, Playwright for user flows and accessibility.
Quick reference: rubric categories touched in this chapter
| Category | Where explained |
|---|---|
| §3 Clean Architecture (enforced) | §4 LayerDependencyTests, DomainPurityTests |
| §4 Domain-Driven Design | §4 AggregateConventionTests factory-method rule |
| §11 Security (test RS256 keypair) | §3 JwtTokenGenerator design note |
| §14 Testability & Test Strategy | §1 CI filter rationale, §2 project layout, §5 integration strategy |
| §17 DevOps & Deployment | §1 two-filter CI rationale |
| §21 Accessibility (a11y) | §3 AssertNoAccessibilityViolationsAsync, §5 E2E strategy |
| §22 Responsive & Cross-Browser | §3 PlaywrightFixture engine selection, §5 E2E browser matrix |
| §28 Front-End Testing & Quality | §2 UI/E2E project layout, §3 Testing.E2E package, §6 bUnit example |
| §30 Compliance, Privacy & Data Governance | §4 PiiConventionTests |
| §32 Dependency & Supply-Chain | §4 DependencyVersionTests, §6 Example A |
| §33 Developer Experience & Inner Loop | §1 MTP filter syntax, self-hosted test binaries |
| §34 Architecture Governance & Documentation | §4 overall fitness-function framing |
Cross-links
- Primer:
00-primer.md#5-the-solution--test-layout, solution files, MTP runner, slnx-excluded UI projects - Primer:
00-primer.md#architecture-enforcement-is-doubled-fitness-functions, compile-time + runtime double enforcement explained - Devops/CI chapter:
devops-cicd.md, CI job definitions, theintegration-testsjob that usesMMCA.ADC.Integration.slnf, theui-e2ejob that builds the Gallery + E2E tests by csproj path - ADRs:
ADRs/003-outbox-dual-dispatch.md, outbox pattern the integration tests exerciseADRs/005-soft-delete-vs-erasure.md, PiiConventionTests rationaleADRs/006-database-per-service.md, per-source DB reset inIIntegrationTestFixtureADRs/007-grpc-extraction.mdandADRs/008-service-extraction-topology.md, MicroserviceExtractionTests rationale