--- name: Backend Testing description: | Backend testing with xUnit, Foundatio.Xunit, integration tests with AppWebHostFactory, FluentClient, ProxyTimeProvider for time manipulation, and test data builders. Keywords: xUnit, Fact, Theory, integration tests, AppWebHostFactory, FluentClient, ProxyTimeProvider, TimeProvider, Foundatio.Xunit, TestWithLoggingBase, test data builders --- # Backend Testing ## Running Tests ```bash # All tests dotnet test # By test name dotnet test --filter "FullyQualifiedName~CanPostUserDescriptionAsync" # By class name dotnet test --filter "ClassName~EventControllerTests" ``` ## Test Folder Structure Tests mirror the source structure: ```text tests/Exceptionless.Tests/ ├── AppWebHostFactory.cs # WebApplicationFactory for integration tests ├── IntegrationTestsBase.cs # Base class for integration tests ├── TestWithServices.cs # Base class for unit tests with DI ├── Controllers/ # API controller tests ├── Jobs/ # Job tests ├── Repositories/ # Repository tests ├── Services/ # Service tests ├── Utility/ # Test data builders │ ├── AppSendBuilder.cs # Fluent HTTP request builder │ ├── DataBuilder.cs # Test data creation │ ├── EventData.cs │ ├── OrganizationData.cs │ ├── ProjectData.cs │ ├── ProxyTimeProvider.cs # Time manipulation │ └── ... └── Validation/ # Validator tests ``` ## Integration Test Base Pattern Inherit from `IntegrationTestsBase` which uses Foundatio.Xunit's `TestWithLoggingBase`: ```csharp // From tests/Exceptionless.Tests/IntegrationTestsBase.cs public abstract class IntegrationTestsBase : TestWithLoggingBase, IAsyncLifetime, IClassFixture { protected readonly TestServer _server; private readonly ProxyTimeProvider _timeProvider; public IntegrationTestsBase(ITestOutputHelper output, AppWebHostFactory factory) : base(output) { _server = factory.Server; _timeProvider = GetService(); } protected TService GetService() where TService : notnull => ServiceProvider.GetRequiredService(); protected FluentClient CreateFluentClient() { var settings = GetService(); return new FluentClient(CreateHttpClient(), new NewtonsoftJsonSerializer(settings)); } } ``` ## Real Test Example From [EventControllerTests.cs](tests/Exceptionless.Tests/Controllers/EventControllerTests.cs): ```csharp public class EventControllerTests : IntegrationTestsBase { private readonly IEventRepository _eventRepository; private readonly IQueue _eventQueue; public EventControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { _eventRepository = GetService(); _eventQueue = GetService>(); } [Fact] public async Task CanPostUserDescriptionAsync() { const string json = "{\"message\":\"test\",\"reference_id\":\"TestReferenceId\"}"; await SendRequestAsync(r => r .Post() .AsTestOrganizationClientUser() .AppendPath("events") .Content(json, "application/json") .StatusCodeShouldBeAccepted() ); var stats = await _eventQueue.GetQueueStatsAsync(); Assert.Equal(1, stats.Enqueued); var processEventsJob = GetService(); await processEventsJob.RunAsync(); await RefreshDataAsync(); var events = await _eventRepository.GetAllAsync(); var ev = events.Documents.Single(e => e.Type == Event.KnownTypes.Log); Assert.Equal("test", ev.Message); } } ``` ## FluentClient Pattern Use `SendRequestAsync` with `AppSendBuilder` for HTTP testing: ```csharp await SendRequestAsync(r => r .Post() .AsTestOrganizationUser() // Basic auth with test user .AppendPath("organizations") .Content(new NewOrganization { Name = "Test" }) .StatusCodeShouldBeCreated() ); // Available auth helpers r.AsGlobalAdminUser() // TEST_USER_EMAIL r.AsTestOrganizationUser() // TEST_ORG_USER_EMAIL r.AsFreeOrganizationUser() // FREE_USER_EMAIL r.AsTestOrganizationClientUser() // API key bearer token ``` ## Test Data Builders Create test data with `CreateDataAsync`: ```csharp var (stacks, events) = await CreateDataAsync(b => b .Event() .TestProject() .Type(Event.KnownTypes.Error) .Message("Test error")); Assert.Single(stacks); Assert.Single(events); ``` ## Time Manipulation with ProxyTimeProvider **NOT `ISystemClock`** — use .NET 8+ `TimeProvider` with `ProxyTimeProvider`: ```csharp // Access via protected property protected ProxyTimeProvider TimeProvider => _timeProvider; // Advance time TimeProvider.Advance(TimeSpan.FromHours(1)); // Set specific time TimeProvider.SetUtcNow(new DateTimeOffset(2024, 1, 15, 12, 0, 0, TimeSpan.Zero)); // Restore to system time TimeProvider.Restore(); ``` Registered in test services: ```csharp services.ReplaceSingleton(_ => new ProxyTimeProvider()); ``` ## Test Principles - **Use real serializer** — Tests use the same JSON serializer as production - **Use real time provider** — Manipulate via `ProxyTimeProvider` when needed - **Refresh after writes** — Call `RefreshDataAsync()` after database changes - **Clean state** — `ResetDataAsync()` clears data between tests ## Foundatio.Xunit Base class provides logging integration: ```csharp using Foundatio.Xunit; public class MyTests : TestWithLoggingBase { public MyTests(ITestOutputHelper output) : base(output) { Log.DefaultLogLevel = LogLevel.Information; } } ```