Esercizio - casi di test con classi di equivalenza e valori limite per il limite di prestiti (progetto originale)
Esercizio originale, nello stile del progetto d'esame (progettazione dei casi di test e test JUnit). Teoria: Software testing - progettazione dei casi di test e JUnitIl testing esegue il programma con dati artificiali per mostrare che soddisfa i requisiti (validation testing) e per scoprirne i difetti (defect testing), ma può rivelare la presenza di errori e non la loro assenza; il test di sviluppo comprende unit test (singole classi e metodi), test dei componenti (le interfacce) e test di sistema (le interazioni); i casi di test si ricavano con le classi di equivalenza (una per partizione) e i valori limite (ai bordi delle partizioni), più linee guida per sequenze e input anomali; JUnit automatizza i test con @Test, fixture @Before/@After (@BeforeClass/@AfterClass), le asserzioni (assertEquals con atteso per primo), eccezioni attese, timeout e @Ignore; nel TDD il test si scrive prima del codice (red, green, refactor) e i test automatici diventano la suite di regressione; seguono i test di rilascio (black box sui requisiti, prestazioni) e dell'utente (alpha, beta, accettazione).Software testing - progettazione dei casi di test e JUnit →; il requisito viene dalla specifica di Esercizio - specifica dei requisiti di un sistema di prestito bibliotecario (progetto originale) (S3.1). Il codice e i test sono stati compilati ed eseguiti con JDK 21 (JUnit 4, con un runner minimo: vedi la nota sul testing).
Testo
Specifica R-23: numero massimo di documenti in prestito contemporaneo.
- L'età valida va da 6 a 120 anni compresi; fuori da questo intervallo il metodo deve lanciare
IllegalArgumentException. - 6-13 anni: 2 documenti; 14-17: 4; 18-64: 6; 65 in su: 8.
- La tessera premium aggiunge 2 documenti in ogni fascia.
La firma è public static int massimo(int eta, boolean premium). Si richiede di: 1) individuare le classi di equivalenza per ciascuna variabile; 2) ricavare i valori limite; 3) scrivere la tabella dei casi di test (id, ingresso, atteso, classe o limite coperto); 4) scrivere i test JUnit 4; 5) verificare con una analisi per mutazione che i test scoprano errori di confine tipici.
Soluzione
1. Classi di equivalenza
| variabile | classi | tipo |
|---|---|---|
eta |
non valida | |
| da a | valida (risultato 2) | |
| da a | valida (4) | |
| da a | valida (6) | |
| da a | valida (8) | |
| non valida | ||
premium |
false |
valida (nessuna aggiunta) |
true |
valida (+2) |
2. Valori limite
I bordi tra le classi sono , , , e (primi valori della classe a destra) e i loro vicini a sinistra: 5, 6, 13, 14, 17, 18, 64, 65, 120, 121. Si provano i valori sul bordo e accanto; per i due estremi non validi bastano 5 e 121.
3. Tabella dei casi
| id | età | premium | atteso | copre |
|---|---|---|---|---|
| T1 | 5 | no | IllegalArgumentException |
non valida bassa, limite 5 |
| T2 | 121 | no | IllegalArgumentException |
non valida alta, limite 121 |
| T3 | 6 | no | 2 | primo valido, limite 6 |
| T4 | 13 | no | 2 | ultimo della fascia 6-13 |
| T5 | 14 | no | 4 | primo della fascia 14-17 |
| T6 | 17 | no | 4 | ultimo della fascia 14-17 |
| T7 | 18 | no | 6 | primo della fascia 18-64 |
| T8 | 64 | no | 6 | ultimo della fascia 18-64 |
| T9 | 65 | no | 8 | primo della fascia 65+ |
| T10 | 120 | no | 8 | ultimo valido, limite 120 |
| T11 | 10 | sì | 4 | premium, fascia 6-13 |
| T12 | 15 | sì | 6 | premium, fascia 14-17 |
| T13 | 40 | sì | 8 | premium, fascia 18-64 |
| T14 | 90 | sì | 10 | premium, fascia 65+ |
Le due classi non valide sono provate da sole (T1, T2) perché un'eccezione nasconderebbe l'esito di altri controlli. I casi premium = sì usano valori interni delle fasce (i bordi sono già coperti con premium = no), perché l'aggiunta di 2 è indipendente dalla fascia: 4 casi bastano a coprire tutte le fasce senza moltiplicare i test. In tutto 14 test, non né 121 (test esaustivo).
4. Codice e test
Il codice sotto test:
/**
* Specifica (R-23): numero massimo di documenti in prestito contemporaneo.
* - eta' valida da 6 a 120 anni compresi, altrimenti IllegalArgumentException;
* - 6..13 anni: 2 documenti; 14..17: 4; 18..64: 6; 65 in su: 8;
* - la tessera premium aggiunge 2 documenti in ogni fascia.
*/
public class LimitePrestiti {
public static int massimo(int eta, boolean premium) {
if (eta < 6 || eta > 120) throw new IllegalArgumentException("eta' non valida: " + eta);
int base;
if (eta <= 13) base = 2;
else if (eta <= 17) base = 4;
else if (eta <= 64) base = 6;
else base = 8;
return premium ? base + 2 : base;
}
}I test JUnit 4:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class LimitePrestitiTest {
// classi non valide: eta' < 6 (limite 5) ed eta' > 120 (limite 121)
@Test(expected = IllegalArgumentException.class)
public void sottoIlMinimo() { LimitePrestiti.massimo(5, false); }
@Test(expected = IllegalArgumentException.class)
public void sopraIlMassimo() { LimitePrestiti.massimo(121, false); }
// classe 6..13
@Test public void primoValido() { assertEquals(2, LimitePrestiti.massimo(6, false)); }
@Test public void ultimoBambino() { assertEquals(2, LimitePrestiti.massimo(13, false)); }
// classe 14..17
@Test public void primoRagazzo() { assertEquals(4, LimitePrestiti.massimo(14, false)); }
@Test public void ultimoRagazzo() { assertEquals(4, LimitePrestiti.massimo(17, false)); }
// classe 18..64
@Test public void primoAdulto() { assertEquals(6, LimitePrestiti.massimo(18, false)); }
@Test public void ultimoAdulto() { assertEquals(6, LimitePrestiti.massimo(64, false)); }
// classe 65..120
@Test public void primoAnziano() { assertEquals(8, LimitePrestiti.massimo(65, false)); }
@Test public void ultimoValido() { assertEquals(8, LimitePrestiti.massimo(120, false)); }
// seconda variabile (premium): un caso per fascia, scelti ai bordi diversi da quelli sopra
@Test public void premiumBambino() { assertEquals(4, LimitePrestiti.massimo(10, true)); }
@Test public void premiumRagazzo() { assertEquals(6, LimitePrestiti.massimo(15, true)); }
@Test public void premiumAdulto() { assertEquals(8, LimitePrestiti.massimo(40, true)); }
@Test public void premiumAnziano() { assertEquals(10, LimitePrestiti.massimo(90, true)); }
}Esecuzione:
LimitePrestitiTest: eseguiti 14, falliti 0, ignorati 0I valori attesi vengono dalla specifica, non dal codice: se il codice fosse sbagliato i test lo direbbero.
5. Analisi per mutazione
Per vedere se i test sono buoni si introducono nel codice errori tipici (mutanti) e si controlla che almeno un test fallisca. Esecuzione reale di cinque mutanti, ciascuno con una sola modifica:
| mutante | modifica | test che falliscono |
|---|---|---|
| M1 | eta <= 17 eta <= 18 |
primoAdulto (18 dà 4 invece di 6) |
| M2 | eta <= 13 eta < 13 |
ultimoBambino (13 dà 4 invece di 2) |
| M3 | eta > 120 eta >= 120 |
ultimoValido (120 lancia l'eccezione) |
| M4 | eta <= 64 eta <= 65 |
primoAnziano (65 dà 6 invece di 8) |
| M5 | base + 2 base + 1 |
quattro test sul premium |
Tutti e cinque i mutanti sono uccisi, e ogni errore di confine è scoperto dal test sul valore limite corrispondente: con soli valori centrali (10, 40, 90) M1-M4 sarebbero sopravvissuti. Un mutante sopravvissuto indicherebbe un test mancante (o un mutante equivalente, che cambia il codice ma non il comportamento).
Versione ripasso
- Specifica R-23: età valida 6-120 (altrimenti
IllegalArgumentException); 6-13 2; 14-17 4; 18-64 6; 65+ 8; premium +2. - Classi per
eta: (non valida), -, -, -, -, (non valida). Perpremium: no, sì. - Valori limite: 5, 6, 13, 14, 17, 18, 64, 65, 120, 121 (sul bordo e accanto). Le classi non valide da sole. Premium con valori interni (10, 15, 40, 90): l'aggiunta è indipendente dalla fascia. In tutto 14 test.
| casi | atteso |
|---|---|
| 5, 121 (no premium) | eccezione |
| 6, 13 | 2 |
| 14, 17 | 4 |
| 18, 64 | 6 |
| 65, 120 | 8 |
| 10, 15, 40, 90 (premium) | 4, 6, 8, 10 |
@Test(expected = IllegalArgumentException.class)
public void sottoIlMinimo() { LimitePrestiti.massimo(5, false); }
@Test public void ultimoBambino() { assertEquals(2, LimitePrestiti.massimo(13, false)); }
@Test public void primoRagazzo() { assertEquals(4, LimitePrestiti.massimo(14, false)); }
@Test public void premiumAdulto() { assertEquals(8, LimitePrestiti.massimo(40, true)); }- Esito:
eseguiti 14, falliti 0. Attesi dalla specifica, non dal codice. - Mutazione: M1
<= 17<= 18: fallisceprimoAdulto; M2<= 13< 13:ultimoBambino; M3> 120>= 120:ultimoValido; M4<= 64<= 65:primoAnziano; M5+ 2+ 1: quattro test premium. Tutti uccisi; con soli valori centrali M1-M4 sopravvivrebbero. Un mutante sopravvissuto = test mancante o mutante equivalente. - Errori tipici: testare solo valori centrali; più classi non valide nello stesso caso; atteso ricavato dal codice; moltiplicare i casi per tutte le combinazioni (20 o 121) invece di scegliere con criterio; dimenticare il limite alto (121).