Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0Crazy-0/ClinicFlow/llms.txt

Use this file to discover all available pages before exploring further.

ClinicFlow maintains three dedicated test projects, each targeting a different layer of the application. Together they enforce an 80 % patch coverage gate on every pull request and feed into both Codecov and SonarCloud quality reporting. This page describes the structure, tooling, and conventions of each project and explains how to run them locally.

Test Project Overview

ClinicFlow.Domain.Tests

Pure unit tests. No mocks, no database, no I/O. Tests entities, value objects, domain services, and policies in total isolation.

ClinicFlow.Application.Tests

Unit tests with mocked infrastructure. MediatR handlers are tested with Moq-mocked repositories and service interfaces. No real database involved.

ClinicFlow.Infrastructure.Tests

Integration tests. EF Core repositories run against a real PostgreSQL container managed by Testcontainers, reset between tests via Respawn.

Running the Tests

Run all three projects with a single command from the repository root:
dotnet test
To generate an OpenCover XML coverage report (required for CI and compatible with Codecov and SonarCloud):
dotnet test --collect:"XPlat Code Coverage" \
  -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
To run a single test project in isolation:
dotnet test ClinicFlow.Domain.Tests
dotnet test ClinicFlow.Application.Tests
dotnet test ClinicFlow.Infrastructure.Tests

ClinicFlow.Domain.Tests

Framework: xUnit v3 · Assertions: AwesomeAssertions · Mocking: Moq · Time: Microsoft.Extensions.TimeProvider.Testing Domain tests verify the core business logic in complete isolation from infrastructure. The directory structure mirrors the domain model:
ClinicFlow.Domain.Tests/
├── Entities/          # Tests for aggregate root behaviour and invariants
├── ValueObjects/      # Tests for value object creation rules and equality
├── Services/
│   ├── Policies/      # Domain policy unit tests
│   ├── Registration/  # User/Doctor/Patient registration domain service tests
│   ├── Rescheduling/  # Appointment rescheduling service tests
│   └── Scheduling/    # Appointment scheduling service tests
└── Shared/            # EntityTestExtensions helper

Shared: EntityTestExtensions

The EntityTestExtensions static class provides a reflection-based helper for setting private entity IDs in tests — necessary because entity Id properties have no public setter:
public static class EntityTestExtensions
{
    public static void SetId(this BaseEntity entity, Guid id)
    {
        var property = typeof(BaseEntity).GetProperty(
            nameof(BaseEntity.Id),
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic
        );

        property?.SetValue(entity, id);
    }
}
Usage in tests:
var appointment = Appointment.Schedule(/* ... */);
appointment.SetId(Guid.NewGuid());

ClinicFlow.Application.Tests

Framework: xUnit v3 · Assertions: AwesomeAssertions · Mocking: Moq · Time: Microsoft.Extensions.TimeProvider.Testing Application tests focus on MediatR command and query handlers. Each handler is instantiated directly with Moq-mocked repository and service dependencies, so no DI container or database is involved. The directory structure mirrors the application feature folders:
ClinicFlow.Application.Tests/
├── AppointmentTypes/
│   ├── Commands/      # One test class per command handler
│   └── Queries/       # One test class per query handler
├── Appointments/
├── Behaviors/         # ValidationBehavior pipeline tests
├── ClinicalFormTemplates/
├── Doctors/
├── MedicalRecords/
├── MedicalSpecialties/
├── Patients/
├── Penalties/
├── Schedules/
├── Users/
└── Shared/            # EntityTestExtensions (same helper as Domain.Tests)
The ValidationBehaviorTests in Behaviors/ verify the MediatR pipeline behaviour: it passes through when validation succeeds, and throws ClinicFlow.Application.Exceptions.ValidationException when FluentValidation returns failures.
[Fact]
public async Task Handle_ShouldThrowValidationException_WhenValidationFails()
{
    // Arrange
    var validatorMock = new Mock<IValidator<DummyRequest>>();
    var validationFailure = new ValidationFailure("Value", "Value is invalid");
    validatorMock
        .Setup(v => v.ValidateAsync(
            It.IsAny<ValidationContext<DummyRequest>>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(new ValidationResult([validationFailure]));

    var validators = new List<IValidator<DummyRequest>> { validatorMock.Object };
    var _sut = new ValidationBehavior<DummyRequest, Unit>(validators);
    var request = new DummyRequest { Value = "Invalid" };
    var nextDelegateMock = new Mock<RequestHandlerDelegate<Unit>>();

    // Act
    var act = async () =>
        await _sut.Handle(
            request,
            nextDelegateMock.Object,
            TestContext.Current.CancellationToken
        );

    // Assert
    var exception = await act.Should().ThrowAsync<ValidationException>();
    exception.Which.Errors.Should().ContainKey("Value");
}

ClinicFlow.Infrastructure.Tests

Framework: xUnit v3 · Assertions: AwesomeAssertions · Database: Testcontainers (PostgreSQL 17 Alpine) · Reset: Respawn Infrastructure tests verify that EF Core repository implementations produce correct SQL, respect global query filters, and handle edge cases against a real PostgreSQL engine — not an in-memory provider.
ClinicFlow.Infrastructure.Tests/
├── Persistence/
│   ├── Repositories/   # One test class per repository
│   └── Seeding/        # DbSeeder integration tests
├── Shared/
│   └── PostgresFixture.cs
└── UnitOfWorkTests.cs

PostgresFixture

PostgresFixture is an IAsyncLifetime fixture shared across repository test classes via xUnit v3 class fixtures. It manages the full container lifecycle:
public class PostgresFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _dbContainer =
        new PostgreSqlBuilder("postgres:17-alpine").Build();

    public ApplicationDbContext Context { get; private set; } = null!;
    public DbConnection DbConnection { get; private set; } = null!;
    public Respawner Respawner { get; private set; } = null!;

    public async ValueTask InitializeAsync()
    {
        await _dbContainer.StartAsync();

        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(_dbContainer.GetConnectionString())
            .Options;

        Context = new ApplicationDbContext(options);
        await Context.Database.EnsureCreatedAsync(); // creates schema from EF Core model

        DbConnection = Context.Database.GetDbConnection();
        await DbConnection.OpenAsync();

        Respawner = await Respawner.CreateAsync(
            DbConnection,
            new RespawnerOptions { DbAdapter = DbAdapter.Postgres, SchemasToInclude = ["public"] }
        );
    }

    public async ValueTask DisposeAsync()
    {
        GC.SuppressFinalize(this);
        await Context.DisposeAsync();
        await _dbContainer.StopAsync();
        await _dbContainer.DisposeAsync();
    }
}
Each test class calls await Respawner.ResetAsync(DbConnection) before or after each test to truncate all tables, ensuring test isolation without restarting the container.

CI Pipeline

The CI pipeline is defined in .github/workflows/ci.yml and consists of two jobs:
  1. Checks out the repository with full history (fetch-depth: 0) for accurate SonarCloud blame data.
  2. Installs JDK 17 and .NET 10.
  3. Installs dotnet-sonarscanner globally.
  4. Begins SonarCloud analysis, pointing at the OpenCover report path.
  5. Restores and builds the solution.
  6. Runs all tests with XPlat Code Coverage in OpenCover format.
  7. Uploads the coverage report to Codecov.
  8. Ends the SonarCloud analysis, publishing results.
Runs after build completes successfully. Installs dotnet-stryker locally to ./tools/.
  • On main pushes: runs Stryker with --version main to save a new baseline to the Stryker Dashboard.
  • On pull requests: runs Stryker with --with-baseline:<base_branch> to compare mutation scores against the saved baseline.
Stryker is run independently for each of the three test projects. HTML mutation reports are uploaded as a GitHub Actions artifact.

Coverage Policy

Codecov

The codecov.yml at the repository root configures the following policy:
coverage:
  status:
    patch:
      default:
        target: 80%
        informational: false
A patch coverage below 80 % on any pull request fails the check and blocks merge. The domain and application flags track per-layer coverage separately. Files excluded from coverage analysis:
  • **/Migrations/** — EF Core migration history
  • **/*.Designer.cs — EF Core migration designer scaffolding
  • tests/**/* — test project files themselves
  • **/Events/** — domain event records (data carriers, no logic)
  • ClinicFlow.Infrastructure/DependencyInjection.cs and ClinicFlow.Application/DependencyInjection.cs — DI registration boilerplate

SonarCloud Quality Gate (new code)

MetricRequired Threshold
Coverage on new code≥ 80 %
Duplicated lines on new code≤ 3 %
New BugsRating A
New VulnerabilitiesRating A
New Security HotspotsRating A
New Code SmellsRating A
The SonarCloud Quality Gate is a required check and cannot be bypassed.
ClinicFlow deliberately avoids [ExcludeFromCodeCoverage] attributes and // NOSONAR comments throughout the Domain and Application layers. These tooling-specific annotations would pollute the domain model. When a coverage check fails on code that contains no testable business logic, the PR author and reviewer verify the false-positive and merge with explicit approval.
Two categories of false positives are officially recognised and permitted:1. EF Core parameterless constructors — entities require a private/protected no-arg constructor for ORM materialisation. Modifying these constructors (e.g., converting {} to expression-body => syntax) causes Codecov to flag the changed lines as uncovered. This is expected and safe to ignore.
// Private constructor for ORM compatibility — not invoked in tests
private Appointment() => TimeRange = null!;
2. Application DTOs and command recordsIRequest<T> records carry no behaviour. If a PR introduces or modifies a command/query record without a test that instantiates it directly, Codecov will report missing coverage. This is acceptable.
public sealed record CancelAppointmentByDoctorCommand(
    Guid AppointmentId,
    Guid InitiatorUserId,
    string? Reason
) : IRequest;
Before merging, the author and reviewer must confirm that only these false-positive categories are missing — all actual business logic must be covered.

Build docs developers (and LLMs) love