Property-Based Testing: un approccio rivoluzionario al testing del software
Nel mondo dello sviluppo software, il testing è una delle attività più critiche per garantire la qualità del prodotto. Tuttavia, i test tradizionali basati su esempi specifici spesso lasciano scoperte molte situazioni impreviste.
Ad esempio, per testare la proprietà commutativa dell’addizione, potremmo scrivere test basati su casi specifici:
[Theory] [InlineData(4, 5)] [InlineData(3, 2)] public void Can_Add_Respect_The_Commutative_Property(int number1, int number2) { var result1 = Calculator.Calculator.Add(number1, number2); var result2 = Calculator.Calculator.Add(number2, number1); Assert.Equal(result1, result2); }
Questo test verifica solo alcuni casi specifici (4+5 e 3+2), non garantisce che la funzione, o il metodo, valga per tutti i numeri possibili. Questa limitazione porta a una domanda fondamentale: è possibile testare in modo più efficace senza scrivere infiniti casi di test manualmente?
Qui entra in gioco il Property-Based Testing (PBT), un approccio innovativo che cambia radicalmente il modo in cui pensiamo al testing. In questo articolo esploreremo i concetti fondamentali del PBT, i suoi vantaggi e come applicarlo con strumenti pratici.
Il contesto: perché PBT?
I test tradizionali (Example-Based Testing) si basano su un insieme finito di input definiti dallo sviluppatore. Questo approccio presenta alcune limitazioni:
- Copertura limitata: i test coprono solo i casi specifici definiti.
- Mancata scoperta di edge case: situazioni limite o non previste possono facilmente sfuggire.
Il Property-Based Testing adotta un approccio diverso: invece di concentrarsi su input specifici, si definiscono proprietà generali che devono valere per un’ampia gamma di input.
Esempio pratico: consideriamo una funzione di ordinamento. Una proprietà generale potrebbe essere: “Se ordino una lista, il risultato deve essere ordinato e contenere gli stessi elementi della lista originale”. Questa regola è valida indipendentemente dagli input specifici.
Come funziona il Property-Based Testing?
Il PBT si basa su tre concetti fondamentali:
- Proprietà: regole generali che il sistema deve rispettare.
- Generazione di input casuali: gli strumenti di PBT generano automaticamente una vasta gamma di input casuali per testare le proprietà.
- Shrinkage: in caso di fallimento, gli strumenti riducono l’input problematico al caso più semplice che causa l’errore, facilitando il debugging.
Flusso di lavoro tipico
- Definisci le proprietà da verificare.
- Usa uno strumento di PBT per generare input casuali.
- Analizza i risultati dei test e risolvi eventuali problemi.
Example-Based Testing vs. Property-Based Testing
Example-Based Testing | Property-Based Testing |
---|---|
Si testano casi specifici definiti manualmente. | Si testano proprietà generali applicabili a qualsiasi input. |
Copertura limitata. | Copertura più ampia grazie alla generazione automatica di input. |
Difficile scoprire edge case. | Edge case spesso scoperti automaticamente. |
Necessita di molti test specifici. | Un singolo test può coprire molti casi. |
Vantaggi del Property-Based Testing
- Maggiore copertura: testa automaticamente un numero elevato di casi, spesso includendo edge case non previsti.
- Individuazione di bug nascosti: scopre errori che potrebbero sfuggire ai test tradizionali.
- Manutenzione ridotta: una singola proprietà ben definita può coprire decine di casi di test manuali.
- Adatto a sistemi complessi: ideale per testare algoritmi, librerie matematiche o protocolli.
Esempi pratici in C# con FsCheck
Di seguito vengono riportati due esempi pratici in C# di Property-Based Testing utilizzando [FsCheck (https://fscheck.github.io/FsCheck/), libreria per il Property-Based Testing in .NET.
- Addizione: una semplice funzione di addizione.
- FizzBuzz: un esercizio comune per testare la logica condizionale.
Addizione
Le proprietà fondamentali dell’addizione sono:
- Commutativa: a + b = b + a
- Associativa: a + (b + c) = (a + b) + c
- Esistenza dell’elemento neutro: a + 0 = a
Codice:
namespace Calculator; public static class Calculator { public static int Add(int a, int b) { return a + b; } }
Unit test:
using FsCheck; using FsCheck.Xunit; namespace CalculatorUnitTests; public class PropertyBasedTestCalculator { [Property(Arbitrary = [typeof(RandomNumberGenerator)])] public Property AddRespectsCommutativityProperty(int a, int b) { var result1 = Calculator.Calculator.Add(a, b); var result2 = Calculator.Calculator.Add(b, a); return (result1 == result2).ToProperty(); } [Property(Arbitrary = [typeof(RandomNumberGenerator)])] public Property AddRespectsAssociativityProperty(int a, int b, int c) { var result1 = Calculator.Calculator.Add(a, Calculator.Calculator.Add(b, c)); var result2 = Calculator.Calculator.Add(Calculator.Calculator.Add(a, b), c); return (result1 == result2).ToProperty(); } [Property(Arbitrary = [typeof(RandomNumberGenerator)])] public Property AddRespectsIdentityProperty(int a) { var result = Calculator.Calculator.Add(a, 0); return (result == a).ToProperty(); } }
Generatori:
using FsCheck; namespace CalculatorUnitTests; public static class RandomNumberGenerator { public static Arbitrary<int> Generate() { return Arb.Default.Int32().Filter(n => n > 0); } }
FizzBuzz
Le proprietà fondamentali per FizzBuzz sono:
- I multipli di tre restituiscono `Fizz`
- I multipli di cinque restituiscono `Buzz`
- I multipli di tre e cinque restituiscono `FizzBuzz`
- Gli altri numeri restituiscono il numero stesso
Codice:
namespace FizzBuzz; public static class FizzBuzz { public static string CheckNumber(int number) { if (number % 3 == 0 && number % 5 == 0) return "FizzBuzz"; if (number % 5 == 0) return "Fizz"; return number % 3 == 0 ? "Buzz" : number.ToString(); } }
Unit test:
using FsCheck; using FsCheck.Xunit; namespace FizzBuzzUnitTests; public class FizzBuzzPropertyBasedTests { [Property(Arbitrary = [typeof(RandomMultipleOfThreeGenerator)])] public Property MultiplesOfThreeShouldBeReplacedWithFizz(int numberToCheck) { return FizzBuzz.FizzBuzz.ChkNumber(numberToCheck).Contains("Fizz").ToProperty(); } [Property(Arbitrary = [typeof(RandomMultipleOfFiveGenerator)])] public Property MultiplesOfFiveShouldBeReplacedWithBuzz(int numberToCheck) { return FizzBuzz.FizzBuzz.ChkNumber(numberToCheck).Contains("Buzz").ToProperty(); } [Property(Arbitrary = [typeof(RandomMultipleOfThreeAndFiveGenerator)])] public Property MultiplesOfThreeAndFiveShouldBeReplacedWithFizzBuzz(int numberToCheck) { return FizzBuzz.FizzBuzz.ChkNumber(numberToCheck).Contains("FizzBuzz").ToProperty(); } [Property(Arbitrary = [typeof(RandomNonMultipleOfThreeOrFiveGenerator)])] public Property NonMultiplesOfThreeAndFiveShouldBeReplacedWithTheNumberItself(int numberToCheck) { return FizzBuzz.FizzBuzz.ChkNumber(numberToCheck).Equals(numberToCheck.ToString()).ToProperty(); } }
Generatori:
using FsCheck; namespace FizzBuzzUnitTests; public static class RandomMultipleOfThreeGenerator { public static Arbitrary<int> Generate() { return Arb.Default.Int32().Filter(n => n is >= 1 and <= 100 && n % 3 == 0); } } public static class RandomMultipleOfFiveGenerator { public static Arbitrary<int> Generate() { return Arb.Default.Int32().Filter(n => n is >= 1 and <= 100 && n % 5 == 0); } } public static class RandomMultipleOfThreeAndFiveGenerator { public static Arbitrary<int> Generate() { return Arb.Default.Int32().Filter(n => n is >= 1 and <= 100 && n % 15 == 0); } } public static class RandomNonMultipleOfThreeOrFiveGenerator { public static Arbitrary<int> Generate() { return Arb.Default.Int32().Filter(n => n is >= 1 and <= 100 && n % 3 != 0 && n % 5 != 0); } }
Sfide e limiti del Property-Based Testing
- Definizione delle proprietà: identificare proprietà utili e significative può essere complesso.
- Complessità iniziale: richiede una curva di apprendimento per comprendere e utilizzare gli strumenti.
- Performance: l’esecuzione di test su input casuali può essere più lenta rispetto ai test tradizionali.
Come superare le sfide?
- Inizia con proprietà semplici: prova a formalizzare le regole di base del sistema.
- Combina Example-Based e Property-Based Testing: usa entrambi per un testing più efficace.
- Affidati agli strumenti di shrinkage: aiutano a isolare gli errori rapidamente.
Quando (e quando non) usare PBT
Ideale per:
- Algoritmi complessi.
- Sistemi con regole ben definite.
- Librerie matematiche o funzioni pure.
Meno indicato per:
- Sistemi con interfacce grafiche.
- Applicazioni in cui è difficile formalizzare le proprietà.
Conclusione
Il Property-Based Testing rappresenta un approccio potente per migliorare la qualità del software, superando i limiti dei test tradizionali. Grazie alla sua capacità di generare automaticamente una vasta gamma di input e di identificare edge case, il PBT è uno strumento essenziale per gli sviluppatori che lavorano su sistemi complessi.