Backend Testing
Test Naming Standards
Pattern: MethodUnderTest_Scenario_ExpectedBehavior
- MethodUnderTest — The actual method on the class being tested, not necessarily the entry point you call.
- Scenario — The input, state, or condition being tested.
- ExpectedBehavior — What the method should do or return.
// ✅ Good
[Fact]
public void GetValue_JObjectWithUserInfo_ReturnsTypedUserInfo() { }
[Fact]
public async Task PostEvent_WithValidPayload_ReturnsAccepted() { }
// ❌ Bad: Vague or wrong method name
[Fact]
public void TestGetValue() { }
[Fact]
public void Deserialize_EmptyArray_ReturnsEmptyList() { } // Wrong: name the method under test, not the entry point
Test Folder Structure
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
├── Api/ # Minimal API tests, organized by production layer
│ ├── Endpoints/ # HTTP integration tests by endpoint family
│ ├── Filters/ # Endpoint filter unit tests
│ ├── Handlers/ # Mediator handler unit tests
│ └── Results/ # API result mapping 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
│ ├── ProxyTimeProvider.cs # Time manipulation
│ └── ...
└── Validation/ # Validator tests
Integration Test Base
Inherit from IntegrationTestsBase (extends Foundatio.Xunit's TestWithLoggingBase):
public abstract class IntegrationTestsBase : TestWithLoggingBase, IAsyncLifetime, IClassFixture<AppWebHostFactory>
Key members: GetService<T>(), CreateFluentClient(), SendRequestAsync(), RefreshDataAsync(), ResetDataAsync(), TimeProvider (ProxyTimeProvider).
HTTP Test Pattern
Use SendRequestAsync with AppSendBuilder for HTTP testing:
await SendRequestAsync(r => r
.Post()
.AsTestOrganizationUser()
.AppendPath("organizations")
.Content(new NewOrganization { Name = "Test" })
.StatusCodeShouldBeCreated()
);
Auth helpers: AsGlobalAdminUser(), AsTestOrganizationUser(), AsFreeOrganizationUser(), AsTestOrganizationClientUser() (API key bearer token).
Test Data Builders
var (stacks, events) = await CreateDataAsync(b => b
.Event()
.TestProject()
.Type(Event.KnownTypes.Error)
.Message("Test error"));
ProxyTimeProvider
NOT ISystemClock — use .NET 8+ TimeProvider with ProxyTimeProvider:
TimeProvider.Advance(TimeSpan.FromHours(1));
TimeProvider.SetUtcNow(new DateTimeOffset(2024, 1, 15, 12, 0, 0, TimeSpan.Zero));
TimeProvider.Restore();
Test Principles
- Regression coverage — Add a focused failing test first when a bug fix can be reproduced cheaply
- Use real serializer — Tests use the same JSON serializer as production
- Refresh after writes — Call
RefreshDataAsync()after database changes - Clean state —
ResetDataAsync()clears data between integration tests