Agent skill

backend-testing

Use this skill when writing or modifying C# tests — unit tests, integration tests, or test fixtures. Covers xUnit patterns, AppWebHostFactory for integration testing, FluentClient for API assertions, ProxyTimeProvider for time manipulation, and test data builders. Apply when adding new test cases, debugging test failures, or setting up test infrastructure.

Stars 2,455
Forks 508

Install this agent skill to your Project

npx add-skill https://github.com/exceptionless/Exceptionless/tree/main/.agents/skills/backend-testing

SKILL.md

Backend Testing

Test Naming Standards

Follow Microsoft's unit testing best practices:

Pattern: MethodUnderTest_Scenario_ExpectedBehavior

  • MethodUnderTest — The actual method on the class being tested, not necessarily the entry point you call. For example, when testing ObjectToInferredTypesConverter, use Read (the converter's method) even though you invoke it via _serializer.Deserialize().
  • Scenario — The input, state, or condition being tested.
  • ExpectedBehavior — What the method should do or return.
csharp
// ✅ Good: Clear method, scenario, and expected behavior
[Fact]
public void GetValue_JObjectWithUserInfo_ReturnsTypedUserInfo() { }

[Fact]
public void GetValue_MissingKey_ThrowsKeyNotFoundException() { }

[Fact]
public void Read_EmptyArray_ReturnsEmptyList() { }  // Tests ObjectToInferredTypesConverter.Read()

[Fact]
public void Write_NestedDictionary_SerializesCorrectly() { }  // Tests ObjectToInferredTypesConverter.Write()

[Fact]
public async Task PostEvent_WithValidPayload_ReturnsAccepted() { }

// ❌ Bad: Vague or missing context
[Fact]
public void TestGetValue() { }

[Fact]
public void CanGetValue() { }

[Fact]
public void Deserialize_EmptyArray_ReturnsEmptyList() { }  // Wrong: Deserialize is the entry point, not the method under test

Running Tests

bash
# All tests
dotnet test

# By test name
dotnet test --filter "FullyQualifiedName~PostEvent_WithValidPayload_ReturnsAccepted"

# 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<AppWebHostFactory>
{
    protected readonly TestServer _server;
    private readonly ProxyTimeProvider _timeProvider;

    public IntegrationTestsBase(ITestOutputHelper output, AppWebHostFactory factory) : base(output)
    {
        _server = factory.Server;
        _timeProvider = GetService<ProxyTimeProvider>();
    }

    protected TService GetService<TService>() where TService : notnull
        => ServiceProvider.GetRequiredService<TService>();

    protected FluentClient CreateFluentClient()
    {
        var settings = GetService<JsonSerializerOptions>();
        return new FluentClient(CreateHttpClient(), new JsonContentSerializer(settings));
    }
}

Real Test Example

From EventControllerTests.cs:

csharp
public class EventControllerTests : IntegrationTestsBase
{
    private readonly IEventRepository _eventRepository;
    private readonly IQueue<EventPost> _eventQueue;

    public EventControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory)
    {
        _eventRepository = GetService<IEventRepository>();
        _eventQueue = GetService<IQueue<EventPost>>();
    }

    [Fact]
    public async Task PostEvent_WithValidPayload_EnqueuesAndProcessesEvent()
    {
        // Arrange
        /* language=json */
        const string json = """{"message":"test","reference_id":"TestReferenceId"}""";

        // Act
        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<EventPostsJob>();
        await processEventsJob.RunAsync();
        await RefreshDataAsync();

        // Assert
        var events = await _eventRepository.GetAllAsync();
        var ev = events.Documents.Single(e => e.Type == Event.KnownTypes.Log);
        Assert.Equal("test", ev.Message);
    }
}

Test Structure (Arrange-Act-Assert)

Use clear // Arrange, // Act, // Assert comments for readability:

csharp
[Fact]
public void GetValue_DirectUserInfoType_ReturnsTypedValue()
{
    // Arrange
    var userInfo = new UserInfo("test@example.com", "Test User");
    var data = new DataDictionary { { "user", userInfo } };

    // Act
    var result = data.GetValue<UserInfo>("user", _jsonOptions);

    // Assert
    Assert.NotNull(result);
    Assert.Equal("test@example.com", result.Identity);
    Assert.Equal("Test User", result.Name);
}

JSON String Literals

Use /* language=json */ comment before JSON strings for IDE syntax highlighting and validation:

csharp
[Fact]
public void GetValue_JsonStringWithError_ReturnsTypedError()
{
    // Arrange
    /* language=json */
    const string json = """{"message":"Test error","type":"System.Exception"}""";
    var data = new DataDictionary { { "@error", json } };

    // Act
    var result = data.GetValue<Error>("@error", _jsonOptions);

    // Assert
    Assert.NotNull(result);
    Assert.Equal("Test error", result.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<TimeProvider>(_ => new ProxyTimeProvider());

Test Principles

  • TDD workflow — When fixing bugs or adding features, write a failing test first
  • 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 stateResetDataAsync() 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;
    }
}

Expand your agent's capabilities with these related and highly-rated skills.

exceptionless/Exceptionless

foundatio-repositories

2,455 508
Explore
exceptionless/Exceptionless

releasenotes

Generate formatted changelogs from git history since the last release tag. Use when preparing release notes that categorize changes into breaking changes, features, fixes, and other sections.

2,455 508
Explore
exceptionless/Exceptionless

e2e-testing

Use this skill when writing or running end-to-end browser tests with Playwright. Covers Page Object Model patterns, selector strategies (data-testid, getByRole, getByLabel), fixtures, and accessibility audits with axe-playwright. Apply when adding E2E test coverage, debugging flaky tests, or testing user flows through the browser.

2,455 508
Explore
exceptionless/Exceptionless

tanstack-query

Use this skill when fetching data, managing server state, or handling API mutations in the Svelte frontend. Covers createQuery, createMutation, query keys, cache invalidation, optimistic updates, and WebSocket-driven refetching. Apply when adding API calls, managing loading/error states, or coordinating cache updates after mutations.

2,455 508
Explore
exceptionless/Exceptionless

dogfood

Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to "dogfood", "QA", "exploratory test", "find issues", "bug hunt", "test this app/site/platform", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.

2,455 508
Explore
exceptionless/Exceptionless

storybook

Use this skill when creating or updating Storybook stories for Svelte components. Covers Svelte CSF story format, defineMeta, argTypes, snippet-based customization, and autodocs. Apply when adding visual documentation for components, setting up story files, or running Storybook for development.

2,455 508
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results