Passa al contenuto principale

Scrivere un test

Struttura di una classe di test​

Una classe di test estende IntegrationTestBase e dichiara i servizi che usa come proprietà risolte dallo scope. Nessun new: i servizi vengono dal container DI, con le stesse dipendenze di produzione.

[TestFixture]
public class CreaOrdineTests : IntegrationTestBase
{
// Ogni accesso risolve il servizio dallo scope del test corrente
private ICreaOrdine CreaOrdine => Get<ICreaOrdine>();

[Test]
public async Task Esegui_con_dati_validi_crea_ordine()
{
// Act
var result = await CreaOrdine.ExecuteAsync(new CreaOrdineCommand(ClienteId: 1, Importo: 250.00m));

// Assert
result.IsSuccess.Should().BeTrue();

var ordine = await Db.Ordini.SingleAsync();
ordine.ClienteId.Should().Be(1);
ordine.Importo.Should().Be(250.00m);
ordine.Stato.Should().Be(StatoOrdine.InAttesa);
}

[Test]
public async Task Esegui_con_cliente_inesistente_fallisce()
{
var result = await CreaOrdine.ExecuteAsync(new CreaOrdineCommand(ClienteId: 999, Importo: 100m));

result.IsSuccess.Should().BeFalse();
result.Error.Should().Be("Cliente non trovato.");
}
}

Db e CreaOrdine provengono dallo stesso scope: condividono il DbContext e le sue transazioni, esattamente come avviene durante una richiesta HTTP in produzione.

SeedAsync​

SeedAsync viene chiamato dalla classe base dopo la creazione dello scope, prima di ogni test. Si sovrascrive per inserire i dati che il test presuppone.

protected override async Task SeedAsync(AppDbContext db)
{
db.Clienti.Add(new Cliente { Id = 1, Nome = "Acme Srl", Email = "acme@example.com" });
db.Prodotti.AddRange(
new Prodotto { Id = 1, Nome = "Widget A", Prezzo = 10.00m, Scorte = 100 },
new Prodotto { Id = 2, Nome = "Widget B", Prezzo = 25.00m, Scorte = 50 }
);
await db.SaveChangesAsync();
}

Poiché ogni test parte da un clone del template vuoto, il seed viene ricreato da zero per ogni test: non c'è mai stato residuo da test precedenti.

FluentAssertions​

FluentAssertions rende le asserzioni leggibili e i messaggi di errore descrittivi.

// Valori semplici
result.IsSuccess.Should().BeTrue();
ordine.Stato.Should().Be(StatoOrdine.Confermato);
ordine.DataCreazione.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));

// Collezioni
ordine.Righe.Should().HaveCount(3);
ordine.Righe.Should().AllSatisfy(r => r.Importo.Should().BeGreaterThan(0));
ordine.Righe.Select(r => r.ProdottoId).Should().BeEquivalentTo([1, 2, 3]);

// Eccezioni
var act = async () => await CreaOrdine.ExecuteAsync(commandNonValido);
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*importo*");

// Oggetti (confronto strutturale, escludendo campi generati)
risposta.Should().BeEquivalentTo(atteso, options => options.Excluding(x => x.Id));

Parallelismo​

I test all'interno della stessa fixture girano in sequenza: ogni test ha il suo scope e il suo DB, ma condividono il template.

Fixture diverse possono girare in parallelo: ciascuna ha il proprio DB e il proprio scope. Il template è condiviso in sola lettura: la creazione è idempotente grazie al check sull'esistenza.

// AssemblyInfo.cs o in cima a qualsiasi file di test
[assembly: Parallelizable(ParallelScope.Fixtures)]