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
- Què és una prova unitària i per què importa
xUnit: instal·lació i alternatives (MSTest,NUnit)- Estructura Arrange-Act-Assert
[Fact]i[Theory]amb[InlineData]- Dobles de prova: mocks amb Moq per a
IRepositoriBiblioteca - Cobertura de codi: què mesura i els seus límits
- Exemple: proves sobre
Prestec.RegistrarDevolucio() - Exemple: proves sobre
Biblioteca.PrestarLlibreAsyncamb un mock del repositori
- 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.
xUnit: instal·lació i alternatives (MSTest, NUnit)
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.csprojdotnet 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.
- 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
Llibreprestat i unPrestecacabat 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
Assertde 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.
[Fact] i [Theory] amb [InlineData]
[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.
- Dobles de prova: mocks amb Moq per a
IRepositoriBiblioteca
IRepositoriBibliotecaUn 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.
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 realMock<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.
- 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.
- Exemple: proves sobre
Prestec.RegistrarDevolucio()
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.
- Exemple: proves sobre
Biblioteca.PrestarLlibreAsync amb un mock del repositori
Biblioteca.PrestarLlibreAsync amb un mock del repositoriRetomant 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
Actseguits de diversosAssertsense 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 dePrestarLlibreAsync): 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
-
Completa la prova parametritzada
CalcularSancio_AmbSancioFixa_RetornaElValorEsperatde l'apartat 4 perquè comprovi, ambAssert.Equal, el valor exacte esperat en cada cas (necessitaràs simular el pas dels dies d'alguna manera, per exemple construintPrestecamb una data de préstec ja passada si la teva implementació ho permet, o documentant la limitació si no). -
Escriu una prova
[Fact]que comprovi queBiblioteca.RegistrarPrestecdispara l'esdevenimentPrestecRegistrat(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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
