# Regla: Pruebas — C# / .NET

Las pruebas en .NET tienen un ecosistema maduro. Estas reglas establecen el
stack estándar del proyecto y cómo usarlo para tests deterministas, legibles
y mantenibles.

---

## xUnit como framework principal

- xUnit es el framework estándar. No usar NUnit ni MSTest en proyectos nuevos.
- Estructura:
  ```
  tests/
  ├── MiProyecto.UnitTests/
  └── MiProyecto.IntegrationTests/
  ```
- Un proyecto de tests por capa — no mezclar unitarios e integración.

---

## NSubstitute para mocks

NSubstitute para todos los mocks. No Moq (historial de cambios de licencia):

```csharp
// Crear y configurar mock
var repo = Substitute.For<IFacturaRepository>();
repo.ObtenerPorIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
    .Returns(new Factura(/* ... */));

// Verificar que fue llamado exactamente una vez
await repo.Received(1).AgregarAsync(
    Arg.Is<Factura>(f => f.Total == 1500m),
    Arg.Any<CancellationToken>());

// Simular error
repo.ObtenerPorIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
    .ThrowsAsync(new TimeoutException());
```

- Mockear solo interfaces y clases abstractas — no clases concretas del dominio.
- NUNCA mockear la clase bajo prueba — señal de acoplamiento excesivo.

---

## FluentAssertions para assertions legibles

```csharp
// MAL — difícil de leer en mensajes de fallo
Assert.Equal(1500m, factura.Total);
Assert.NotNull(factura.Cliente);

// BIEN — mensajes de error descriptivos automáticos
factura.Total.Should().Be(1500m);
factura.Cliente.Should().NotBeNull();
facturas.Should().HaveCount(3)
    .And.ContainSingle(f => f.Estatus == EstadoFactura.Pagada);

// Para objetos complejos — ignora campos no relevantes
resultado.Should().BeEquivalentTo(esperado,
    opts => opts.Excluding(f => f.FechaCreacion));
```

---

## TestContainers para integración

```csharp
public class DatabaseFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithDatabase("testdb").WithUsername("test").WithPassword("test")
        .Build();

    public string ConnectionString => _postgres.GetConnectionString();
    public async Task InitializeAsync() => await _postgres.StartAsync();
    public async Task DisposeAsync() => await _postgres.DisposeAsync();
}

[Collection("Database")]
public class FacturaRepositoryTests : IClassFixture<DatabaseFixture>
{
    public FacturaRepositoryTests(DatabaseFixture db) { /* usar db.ConnectionString */ }

    [Fact]
    public async Task Should_PersistFactura_When_Added() { ... }
}
```

- TestContainers para tests de repositorio contra PostgreSQL real.
- `IClassFixture<T>` para compartir el contenedor entre tests del mismo archivo.

---

## WebApplicationFactory para tests de API

```csharp
public class FacturasApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public FacturasApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
            builder.ConfigureServices(services =>
            {
                services.RemoveAll<AppDbContext>();
                services.AddDbContext<AppDbContext>(opts =>
                    opts.UseInMemoryDatabase("TestDb"));
            }))
            .CreateClient();
    }

    [Fact]
    public async Task Should_Return200_When_FacturaExists()
    {
        var response = await _client.GetAsync($"/facturas/{Guid.NewGuid()}");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}
```

- Reemplazar dependencias de infraestructura (BD, servicios externos) con dobles.
- BD in-memory para tests de API (velocidad), TestContainers para tests de repositorio (fidelidad).

---

## Cobertura mínima 80% con coverlet

```bash
dotnet test --collect:"XPlat Code Coverage"
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:./coverage-report
```

- CI falla si cobertura cae por debajo del 80% en proyectos de lógica de negocio.
- Proyectos de infraestructura y configuración: umbral mínimo del 60%.

---

## Patrón Arrange-Act-Assert

```csharp
[Fact]
public async Task Should_ReturnError_When_ClienteNoExiste()
{
    // Arrange
    _clienteRepo.ExisteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
        .Returns(false);

    // Act
    var resultado = await _handler.Handle(
        new CrearFacturaCommand("test@test.com", 1500m),
        CancellationToken.None);

    // Assert
    resultado.IsFailed.Should().BeTrue();
    resultado.Errors.Should().ContainSingle(e => e.Message.Contains("Cliente"));
}
```

- Nombre del test: `Should_[resultado]_When_[condicion]`.
- Tests parametrizados con `[Theory]` + `[InlineData]` — no duplicar el cuerpo del test.

---

## Checklist de pruebas antes de hacer merge

- [ ] xUnit + NSubstitute + FluentAssertions en todos los tests nuevos
- [ ] Tests nuevos para todo código nuevo de negocio
- [ ] Test de regresión escrito antes del fix para todo bug
- [ ] Cobertura >= 80% verificada con coverlet en CI
- [ ] Sin `Thread.Sleep` en tests — usar `TimeProvider` mockeado
- [ ] Tests de integración con TestContainers o WebApplicationFactory
- [ ] Patrón Arrange-Act-Assert con separación visual clara
- [ ] Naming: `Should_[resultado]_When_[condicion]`
