Testing — Unit & Integration Test Patterns
Granit.Testing eliminates the 50+ lines of boilerplate that every test project
repeats: DI setup, NSubstitute mocks for IClock / ICurrentTenant /
ICurrentUserService / IGuidGenerator, and EF Core context wiring. New test
setup drops to under 10 lines.
Package structure
Section titled “Package structure”DirectoryGranit.Testing/ Fakes, fixture, Bogus generators
- Granit.Testing.EntityFrameworkCore InMemory + SQLite DbContext factories
| Package | Role | Depends on |
|---|---|---|
Granit.Testing | GranitTestFixture, fakes, GranitWebApplicationFactory, Bogus generators | Granit, Granit.Users, Granit.Timing, Granit.Guids, Bogus |
Granit.Testing.EntityFrameworkCore | InMemoryDbContextFactory, SqliteDbContextFactory, AddGranitTestDbContext | Granit.Testing, Granit.Persistence, EF Core InMemory, EF Core Sqlite |
Dependency graph
Section titled “Dependency graph”graph TD
T[Granit.Testing] --> C[Granit]
T --> S[Granit.Users]
T --> Ti[Granit.Timing]
T --> G[Granit.Guids]
TEF[Granit.Testing.EntityFrameworkCore] --> T
TEF --> P[Granit.Persistence]
AsyncLocal-backed fakes
Section titled “AsyncLocal-backed fakes”All fakes store state in AsyncLocal<T>. This means each xUnit test gets
isolated state even when sharing a fixture via IClassFixture<T> — no
cross-test pollution when tests run in parallel.
| Fake | Interface | Default value |
|---|---|---|
FakeCurrentTenant | ICurrentTenant | Unavailable (no tenant) |
FakeCurrentUser | ICurrentUserService | Authenticated user "test-user-001" |
FakeClock | IClock | 2026-01-15T10:00:00Z |
FakeGuidGenerator | IGuidGenerator | Sequential deterministic GUIDs |
FakeCurrentTenant
Section titled “FakeCurrentTenant”FakeCurrentTenant tenant = new();tenant.Id = tenantId;tenant.Name = "Acme Corp";
// Scope-based (restores previous state on Dispose)using (tenant.Change(otherTenantId, "Other Corp")){ // tenant.Id == otherTenantId}// tenant.Id == tenantId (restored)FakeClock
Section titled “FakeClock”FakeClock clock = new(); // 2026-01-15T10:00:00Z
clock.Advance(TimeSpan.FromHours(2)); // Now == 12:00clock.Now = new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero); // explicitFakeGuidGenerator
Section titled “FakeGuidGenerator”// Queue mode: return exact GUIDs for assertionsFakeGuidGenerator gen = new();gen.Enqueue(knownGuid);gen.Create(); // returns knownGuid
// Sequential mode: deterministic GUIDs when queue is emptygen.Create(); // 00000001-0000-0000-0000-000000000000gen.Create(); // 00000002-0000-0000-0000-000000000000GranitTestFixture
Section titled “GranitTestFixture”Bootstraps the full Granit module graph and replaces core services with fakes.
public sealed class CachingTests : IAsyncDisposable{ private readonly GranitTestFixture<GranitCachingModule> _fixture = new();
[Fact] public async Task Cache_Service_Is_Resolved() { await _fixture.BuildAsync();
var cache = _fixture.GetRequiredService<IFusionCache>(); cache.ShouldNotBeNull(); }
// Customize fakes per test (AsyncLocal isolation) [Fact] public async Task Tenant_Aware_Cache() { _fixture.Tenant.Id = Guid.NewGuid(); await _fixture.BuildAsync(); // ... }
public async ValueTask DisposeAsync() => await _fixture.DisposeAsync();}GranitWebApplicationFactory
Section titled “GranitWebApplicationFactory”For endpoint integration tests with a real HTTP pipeline.
public sealed class ApiTests : IClassFixture<GranitWebApplicationFactory<Program>>{ private readonly GranitWebApplicationFactory<Program> _factory;
public ApiTests(GranitWebApplicationFactory<Program> factory) { _factory = factory; }
[Fact] public async Task Get_Returns_Ok() { _factory.User.UserId = "admin-001"; _factory.User.AddRole("Admin");
HttpClient client = _factory.CreateClient(); HttpResponseMessage response = await client.GetAsync("/api/items");
response.StatusCode.ShouldBe(HttpStatusCode.OK); }}EF Core factories
Section titled “EF Core factories”// Real SQL semantics — catches FK violations, type mismatchesusing SqliteDbContextFactory<AppDbContext> factory = new();
using AppDbContext context = factory.CreateContext();context.Orders.Add(new Order { Title = "Test" });await context.SaveChangesAsync();
// Audit fields set automatically by interceptorsentity.CreatedAt.ShouldBe(factory.Clock.Now);// Fast but no SQL translation — use only for interceptor/logic testsInMemoryDbContextFactory<AppDbContext> factory = new();
using AppDbContext context = factory.CreateContext();// ...// Inside GranitTestFixture — registers DbContext in the DI containerawait fixture.BuildAsync(services => services.AddGranitTestDbContext<AppDbContext>());
var context = fixture.GetRequiredService<AppDbContext>();Bogus generators
Section titled “Bogus generators”GranitFaker
Section titled “GranitFaker”Static factory methods for generating realistic fake data.
FakeCurrentUser user = GranitFaker.CurrentUser().Generate();// user.UserId = "a3f1b2c4-...", user.Email = "[email protected]"
FakeCurrentTenant tenant = GranitFaker.Tenant().Generate();// tenant.Id = Guid, tenant.Name = "Acme Corp"Entity audit extensions
Section titled “Entity audit extensions”// Populate audit fields on domain entitiesList<Invoice> invoices = new Faker<Invoice>() .RuleForFullAudit() // Id, CreatedAt/By, ModifiedAt/By, IsDeleted .RuleForMultiTenant(tenantId) // TenantId .RuleFor(i => i.Amount, f => f.Finance.Amount()) .Generate(50);| Extension | Constraint | Fields populated |
|---|---|---|
RuleForCreationAudit<T>() | T : CreationAuditedEntity | Id, CreatedAt, CreatedBy |
RuleForAudit<T>() | T : AuditedEntity | + ModifiedAt, ModifiedBy |
RuleForFullAudit<T>() | T : FullAuditedEntity | + IsDeleted, DeletedAt, DeletedBy |
RuleForMultiTenant<T>() | T : IMultiTenant | TenantId |