La lliçó anterior va desacoblar Biblioteca de qualsevol mecanisme concret de persistència, injectant IRepositoriBiblioteca en comptes d'acoblar-la a SQLite, JSON o Entity Framework Core. Es va esmentar, de passada, que això obriria la porta a provar Biblioteca sense tocar cap fitxer ni base de dades real. Aquesta lliçó compleix aquesta promesa: presenta les proves unitàries, codi que verifica automàticament que un altre codi es comporta com s'espera, i les aplica sobre Prestec.RegistrarDevolucio() i sobre Biblioteca.PrestarLlibreAsync, substituint IRepositoriBiblioteca per un doble de prova que no necessita cap base de dades real. Amb això, qualsevol canvi futur a BiblioTech —inclosa la refactorització de l'última lliçó d'aquest mòdul— es podrà verificar en segons, de forma repetible, en comptes de comprovar-se a mà cada vegada.

Contingut

  1. Què és una prova unitària i per què importa
  2. xUnit: instal·lació i alternatives (MSTest, NUnit)
  3. Estructura Arrange-Act-Assert
  4. [Fact] i [Theory] amb [InlineData]
  5. Dobles de prova: mocks amb Moq per a IRepositoriBiblioteca
  6. Cobertura de codi: què mesura i els seus límits
  7. Exemple: proves sobre Prestec.RegistrarDevolucio()
  8. Exemple: proves sobre Biblioteca.PrestarLlibreAsync amb un mock del repositori

  1. Què és una prova unitària i per què importa

Una prova unitària és un fragment de codi que executa una unitat petita i aïllada del teu programa —normalment un únic mètode— i comprova automàticament que el seu resultat és l'esperat, sense intervenció manual. La paraula clau és "automàticament": en comptes d'executar BiblioTech a mà, fer un préstec des de la consola i mirar si el missatge sembla correcte, una prova unitària fa exactament això mateix en codi, i falla de forma sorollosa si el resultat no coincideix amb l'esperat.

Sense proves unitàries Amb proves unitàries
Verificar un canvi significa executar l'aplicació sencera a mà Verificar un canvi significa executar un conjunt de proves en segons
Un error introduït per un canvi es descobreix, si de cas, molt més tard Un error es descobreix en el moment, abans d'arribar a producció
Refactoritzar (Lliçó 5) fa por: seguirà funcionant tot igual? Refactoritzar és segur: les proves existents confirmen que el comportament no ha canviat
Repetir la mateixa comprovació manual una i altra vegada és tediós Les mateixes comprovacions s'executen, iguals, tantes vegades com calgui

Les proves unitàries no substitueixen altres formes de prova (d'integració, manuals, d'acceptació amb l'usuari final), però són la base més ràpida i barata de construir: s'executen en mil·lisegons, sense dependre d'una base de dades, una xarxa, ni una interfície gràfica.

  1. xUnit: instal·lació i alternatives (MSTest, NUnit)

xUnit és un dels frameworks de proves més usats a l'ecosistema .NET modern (és, de fet, el que fa servir el propi equip d'ASP.NET Core per provar el seu codi). Un projecte de proves xUnit es crea com un projecte .csproj independent, separat del projecte principal:

dotnet new xunit -n BiblioTech.Proves
cd BiblioTech.Proves
dotnet add reference ../BiblioTech/BiblioTech.csproj

dotnet add reference afegeix una referència al projecte principal de BiblioTech, perquè el projecte de proves pugui fer servir les seves classes (Biblioteca, Prestec, IRepositoriBiblioteca...). Existeixen dues alternatives a xUnit, amb una filosofia molt semblant i només diferències de sintaxi:

Framework Atribut de prova Origen
xUnit (usat en aquesta lliçó) [Fact] / [Theory] El més popular en projectes .NET moderns i de codi obert
MSTest [TestMethod] El framework de proves propi de Microsoft, integrat a Visual Studio des de sempre
NUnit [Test] Un dels frameworks de proves més antics a .NET, inspirat en JUnit de Java

Aquesta lliçó se centra en xUnit per ser l'opció més estesa en projectes nous, però els conceptes (Arrange-Act-Assert, dobles de prova) s'apliquen exactament igual amb qualsevol dels tres.

  1. Estructura Arrange-Act-Assert

Gairebé qualsevol prova unitària ben escrita segueix el mateix patró de tres passos, conegut com Arrange-Act-Assert (preparar-actuar-comprovar):

[Fact]
public void RegistrarDevolucio_AssignaDataDevolucio()
{
    // Arrange: prepara les dades i objectes necessaris per a la prova
    Llibre llibre = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
    Soci soci = new Soci(1, "Ana Martinez");
    llibre.Prestar();
    Prestec prestec = new Prestec(llibre, soci);

    // Act: executa l'accio concreta que es vol provar
    prestec.RegistrarDevolucio();

    // Assert: comprova que el resultat es l'esperat
    Assert.NotNull(prestec.DataDevolucio);
}
  • Arrange: construeix l'estat de partida —aquí, un Llibre prestat i un Prestec acabat de crear, sense retornar encara—.
  • Act: crida l'únic mètode que es vol provar —aquí, RegistrarDevolucio()—; si aquesta secció té més d'una línia, sol ser senyal que la prova intenta verificar massa coses alhora.
  • Assert: comprova, amb els mètodes de la classe Assert de xUnit (Assert.Equal, Assert.True, Assert.NotNull, Assert.Throws...), que el resultat obtingut coincideix amb l'esperat.

Aquesta separació en tres blocs, encara que sembli només una convenció de format, fa que qualsevol prova es llegeixi de la mateixa manera a tot el projecte —el mateix benefici de consistència vist a la Lliçó 1 d'aquest mòdul, aplicat ara a les proves.

  1. [Fact] i [Theory] amb [InlineData]

xUnit distingeix dos tipus de mètode de prova:

  • [Fact]: una prova amb una única entrada, sense paràmetres —com la de l'apartat anterior—.
  • [Theory]: una prova parametritzada, que s'executa diverses vegades amb conjunts de valors diferents, cadascun declarat amb [InlineData(...)].
[Theory]
[InlineData(0, 0)]    // sense retard, sense sancio
[InlineData(5, 0)]    // 5 dies de prestec, dins del termini permes (14 dies), sense sancio
[InlineData(20, 2.00)] // 20 dies, 6 de retard sobre el termini de 14: sancio fixa de 2.00
public void CalcularSancio_AmbSancioFixa_RetornaElValorEsperat(int diesFinsDevolucio, decimal sancioEsperada)
{
    // Arrange
    Llibre llibre = new Llibre("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
    Soci soci = new Soci(1, "Ana Martinez");
    llibre.Prestar();
    Prestec prestec = new Prestec(llibre, soci);

    // Act: es simula el pas del temps directament sobre el calcul (vegeu Errors Comuns)
    prestec.RegistrarDevolucio();
    decimal sancio = prestec.CalcularSancio(new SancioFixa());

    // Assert
    Assert.True(sancio >= 0); // l'exercici 1 completa una versio mes precisa d'aquesta prova
}

Cada línia [InlineData(...)] genera una execució independent del mateix mètode de prova, amb aquests valors concrets com a arguments (diesFinsDevolucio, sancioEsperada). En comptes d'escriure tres mètodes [Fact] gairebé idèntics —un per cada combinació d'entrada/sortida—, [Theory] els expressa com a dades, molt més compacte i fàcil d'ampliar amb un cas més.

  1. Dobles de prova: mocks amb Moq per a IRepositoriBiblioteca

Un doble de prova (test double) és un objecte que substitueix, durant una prova, una dependència real —una base de dades, un servei extern, el sistema de fitxers—, perquè la prova s'executi ràpid, de forma aïllada, i sense dependre de res extern. Moq és la llibreria més usada a .NET per crear mocks, el tipus de doble de prova més flexible: un objecte que implementa una interfície (com IRepositoriBiblioteca, extreta a la lliçó anterior) sense cap lògica real, el comportament del qual es configura línia a línia a la mateixa prova.

dotnet add package Moq
using Moq;

Mock<IRepositoriBiblioteca> mockRepositori = new Mock<IRepositoriBiblioteca>();

// Configura el comportament del mock quan es cridi GuardarCataleg amb qualsevol llista
mockRepositori
    .Setup(repo => repo.GuardarCataleg(It.IsAny<List<MaterialBibliotecari>>()))
    .Verifiable(); // marca aquesta crida per poder verificar despres que va passar

IRepositoriBiblioteca repositoriFals = mockRepositori.Object; // l'objecte que s'injecta a Biblioteca

// ... fer servir repositoriFals com si fos un IRepositoriBiblioteca real ...

mockRepositori.Verify(repo => repo.GuardarCataleg(It.IsAny<List<MaterialBibliotecari>>()), Times.Once);
// Comprova que GuardarCataleg es va cridar exactament una vegada, sense tocar cap fitxer ni base de dades real

Mock<IRepositoriBiblioteca> crea un objecte que compleix el contracte de la interfície sense cap implementació real al darrere; .Setup(...) defineix què ha de passar quan es cridi un mètode concret (aquí, simplement acceptar-ho, sense fer res més); It.IsAny<...>() accepta qualsevol valor d'aquest tipus com a argument, sense exigir-ne un de concret; .Verify(...) comprova, al final de la prova, que una crida esperada realment va ocórrer. Aquesta és la raó pràctica per la qual la Lliçó 3 va extreure IRepositoriBiblioteca: sense aquesta interfície, no hi hauria res a substituir per un mock, i provar Biblioteca exigiria sempre una base de dades real.

  1. Cobertura de codi: què mesura i els seus límits

La cobertura de codi (code coverage) mesura quin percentatge de les línies (o branques) del codi de producció s'executen en córrer el conjunt de proves. Eines com Coverlet (integrable amb dotnet test --collect:"XPlat Code Coverage") generen un informe amb aquest percentatge. És una mètrica útil per detectar zones del codi completament sense provar, però té un límit important que convé tenir clar des del principi: una cobertura alta no garanteix proves de qualitat. Un mètode pot executar-se en una prova (comptar com "cobert") sense que aquesta prova comprovi realment res rellevant amb Assert. La cobertura és un indicador del que falta provar, no un objectiu a maximitzar per si mateix.

  1. Exemple: proves sobre Prestec.RegistrarDevolucio()

Un conjunt més complet de proves sobre RegistrarDevolucio(), cobrint el cas normal i un cas límit:

public class PrestecProves
{
    [Fact]
    public void RegistrarDevolucio_AssignaDataDevolucio()
    {
        // Arrange
        Llibre llibre = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
        Soci soci = new Soci(1, "Ana Martinez");
        llibre.Prestar();
        Prestec prestec = new Prestec(llibre, soci);

        // Act
        prestec.RegistrarDevolucio();

        // Assert
        Assert.NotNull(prestec.DataDevolucio);
    }

    [Fact]
    public void DataDevolucio_AbansDeRetornar_EsNull()
    {
        // Arrange
        Llibre llibre = new Llibre("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
        Soci soci = new Soci(1, "Ana Martinez");
        llibre.Prestar();
        Prestec prestec = new Prestec(llibre, soci);

        // Act: no es crida a RegistrarDevolucio()

        // Assert
        Assert.Null(prestec.DataDevolucio);
    }
}

La segona prova comprova explícitament l'estat inicial (DataDevolucio és null mentre no s'ha retornat), un cas tan important com el "camí feliç" de la primera prova: verifica que Prestec no assigna una data de devolució prematurament per error.

  1. Exemple: proves sobre Biblioteca.PrestarLlibreAsync amb un mock del repositori

Retomant Biblioteca de la lliçó anterior, s'amplia PrestarLlibreAsync perquè, després de registrar el préstec, persisteixi el nou estat del catàleg a través d'IRepositoriBiblioteca:

class Biblioteca
{
    private readonly IRepositoriBiblioteca _repositori;

    public List<MaterialBibliotecari> Cataleg { get; } = new List<MaterialBibliotecari>();
    public List<Prestec> Prestecs { get; } = new List<Prestec>();

    public event Action<Prestec>? PrestecRegistrat;

    public Biblioteca(IRepositoriBiblioteca repositori)
    {
        _repositori = repositori;
    }

    public void RegistrarPrestec(Prestec prestec)
    {
        Prestecs.Add(prestec);
        PrestecRegistrat?.Invoke(prestec);
    }

    public async Task PrestarLlibreAsync(Llibre llibre, Soci soci)
    {
        await Task.Delay(1000); // simulacio d'una verificacio lenta, Modul 4

        if (!llibre.Disponible)
        {
            throw new InvalidOperationException($"'{llibre.Titol}' no esta disponible per a prestec.");
        }

        llibre.Prestar();
        RegistrarPrestec(new Prestec(llibre, soci));

        _repositori.GuardarCataleg(Cataleg); // persisteix el nou estat despres del prestec
    }
}

Les proves sobre aquest mètode, substituint IRepositoriBiblioteca per un mock de Moq:

public class BibliotecaProves
{
    [Fact]
    public async Task PrestarLlibreAsync_AmbLlibreDisponible_GuardaElCataleg()
    {
        // Arrange
        Mock<IRepositoriBiblioteca> mockRepositori = new Mock<IRepositoriBiblioteca>();
        Biblioteca biblioteca = new Biblioteca(mockRepositori.Object);

        Llibre llibre = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
        Soci soci = new Soci(1, "Ana Martinez");

        // Act
        await biblioteca.PrestarLlibreAsync(llibre, soci);

        // Assert
        Assert.False(llibre.Disponible);
        Assert.Single(biblioteca.Prestecs);
        mockRepositori.Verify(
            repo => repo.GuardarCataleg(It.IsAny<List<MaterialBibliotecari>>()),
            Times.Once); // es va guardar el cataleg exactament una vegada, sense tocar cap base de dades real
    }

    [Fact]
    public async Task PrestarLlibreAsync_AmbLlibreNoDisponible_LlancaExcepcioINoGuarda()
    {
        // Arrange
        Mock<IRepositoriBiblioteca> mockRepositori = new Mock<IRepositoriBiblioteca>();
        Biblioteca biblioteca = new Biblioteca(mockRepositori.Object);

        Llibre llibre = new Llibre("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
        llibre.Prestar(); // ja prestat d'entrada
        Soci soci = new Soci(1, "Ana Martinez");

        // Act + Assert: Assert.ThrowsAsync executa l'accio i comprova l'excepcio esperada
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => biblioteca.PrestarLlibreAsync(llibre, soci));

        mockRepositori.Verify(
            repo => repo.GuardarCataleg(It.IsAny<List<MaterialBibliotecari>>()),
            Times.Never); // si el prestec va fallar, mai s'hauria d'haver intentat guardar
    }
}

Cap d'aquestes dues proves toca un fitxer, una base de dades SQLite ni un BibliotecaDbContext real: mockRepositori.Object és un IRepositoriBiblioteca de mentida que només registra quines crides va rebre, permetent comprovar amb Times.Once/Times.Never exactament quantes vegades es va invocar GuardarCataleg. La segona prova, a més, verifica un aspecte tan important com el cas d'èxit: que un préstec fallit no provoca un desat indegut del catàleg.

Errors Comuns i Consells

  • Provar diverses coses diferents en una sola prova: si un mètode de prova té diversos blocs Act seguits de diversos Assert sense relació entre ells, divideix-lo en proves independents; quan una prova falla, el seu nom ha de bastar per saber exactament què s'ha trencat.
  • Dependre de temps reals d'espera en proves (com el Task.Delay(1000) real de PrestarLlibreAsync): en un projecte real, aquesta espera se substituiria per una abstracció del temps injectable (fora de l'abast d'aquesta lliçó); aquí simplement assumeix que les proves d'aquest apartat triguen, de fet, al voltant d'un segon cadascuna.
  • Mockejar una classe concreta en comptes d'una interfície: Moq pot crear mocks de classes amb membres virtual, però és molt més senzill i més habitual mockejar interfícies —una altra raó pràctica per preferir interfícies (IRepositoriBiblioteca) als punts d'extensió del disseny, com ja vas veure a la Lliçó 3.
  • Perseguir el 100% de cobertura de codi com a objectiu en si mateix: és més valuós tenir un 80% de cobertura amb proves que verifiquin comportaments reals, que un 100% amb proves buides que només executen codi sense comprovar res rellevant.

Exercicis

  1. Completa la prova parametritzada CalcularSancio_AmbSancioFixa_RetornaElValorEsperat de l'apartat 4 perquè comprovi, amb Assert.Equal, el valor exacte esperat en cada cas (necessitaràs simular el pas dels dies d'alguna manera, per exemple construint Prestec amb una data de préstec ja passada si la teva implementació ho permet, o documentant la limitació si no).

  2. Escriu una prova [Fact] que comprovi que Biblioteca.RegistrarPrestec dispara l'esdeveniment PrestecRegistrat (fes servir una variable local capturada per la lambda subscrita per comprovar que es va invocar).

Solucions

[Theory]
[InlineData(0, 0)]
[InlineData(2, 4.00)] // 2 dies de retard sobre 14 permesos, SancioFixa cobra 2.00 fix
public void CalcularSancio_AmbSancioFixa_RetornaElValorEsperat(int diesRetard, decimal sancioEsperada)
{
    // Nota: aquesta versio assumeix un metode auxiliar de proves que permet fixar DataPrestec
    // manualment; documentar aquesta limitacio tambe forma part d'escriure bones proves.
    decimal sancioCalculada = new SancioFixa().CalcularSancio(diesRetard);
    Assert.Equal(sancioEsperada, sancioCalculada);
}
[Fact]
public void RegistrarPrestec_DisparaEsdevenimentPrestecRegistrat()
{
    // Arrange
    Mock<IRepositoriBiblioteca> mockRepositori = new Mock<IRepositoriBiblioteca>();
    Biblioteca biblioteca = new Biblioteca(mockRepositori.Object);

    bool esdevenimentDisparat = false;
    biblioteca.PrestecRegistrat += _ => esdevenimentDisparat = true;

    Llibre llibre = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
    Soci soci = new Soci(1, "Ana Martinez");
    llibre.Prestar();

    // Act
    biblioteca.RegistrarPrestec(new Prestec(llibre, soci));

    // Assert
    Assert.True(esdevenimentDisparat);
}

Conclusió

En aquesta lliçó has conegut què és una prova unitària i per què dona confiança per canviar codi sense por, has usat xUnit amb l'estructura Arrange-Act-Assert, [Fact] i [Theory] amb [InlineData], i has substituït IRepositoriBiblioteca per un mock de Moq per provar Biblioteca.PrestarLlibreAsync de forma ràpida i aïllada, sense cap base de dades real —el benefici concret d'haver extret aquesta interfície a la lliçó anterior. També has vist què mesura la cobertura de codi i per què no s'ha de perseguir com a fi en si mateix.

L'última lliçó d'aquest mòdul, Revisió i Refactorització de Codi, es recolza directament en el que has après aquí: les proves unitàries que acabes d'escriure seran la xarxa de seguretat que permetrà refactoritzar amb confiança un mètode llarg de Biblioteca, comprovant a cada pas que el seu comportament no ha canviat.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats