Un componente viene testato inviando degli input alla sua interfaccia, attendendo che il componente li elabori ed
infine controllandone i risultati. Nel corso dell'elaborazione un componente probabilmente utilizza altri componenti,
inviando loro degli input ed utilizzandone i risultati:
Fig 1: Test di un componente implementato
Gli altri componenti potrebbero causare dei problemi al test:
-
Potrebbero non essere stati ancora implementati.
-
Potrebbero contenere dei difetti che impediscono il funzionamento dei test o che potrebbero far impiegare molto
tempo nella ricerca dell'errore per poi scoprire che non era causato dal proprio componente.
-
Possono rendere difficile l'esecuzione dei test, quando sono necessari. Se un componente è un database commerciale,
l'azienda potrebbe non disporre di licenze a sufficienza per tutti. Oppure uno dei componenti potrebbe essere
dell'hardware disponibile solo ad orari prestabiliti in un altro laboratorio.
-
Possono rendere la fase di test talmente lenta da impedire l'esecuzione dei test con la frequenza necessaria. Ad
esempio, l'inizializzazione del database potrebbe richiedere cinque minuti a test.
-
Potrebbe essere difficile indurre i componenti a produrre determinati risultati. Ad esempio, si potrebbe volere che
ogni metodo che scrive su disco sia in grado di gestire gli errori di disco pieno. Come si può essere sicuri che il
disco si riempia nel momento esatto in cui viene richiamato quel metodo?
Per evitare questi problemi è possibile scegliere di utilizzare i componenti stub (detti anche oggetti
fittizi). I componenti stub agiscono come dei veri componenti, almeno per quanto riguarda i valori che il
componente in fase di test invia loro mentre esegue le sue verifiche. Possono anche andare oltre: possono essere degli
emulatori a scopo generico che tentano di emulare fedelmente la maggior parte o la totalità delle funzionalità
del componente. Ad esempio, spesso è una buona strategia creare degli emulatori software per l'hardware. Hanno la
stessa funzionalità dell'hardware, solo più lenta. Sono utili perché supportano meglio il debug, ne sono disponibili
più copie e possono essere utilizzati prima che sia terminato l'hardware.
Fig2: Verifica di un componente implementato rendendo stub un componente da cui dipende
Gli stub hanno due svantaggi.
-
Possono essere costosi da creare. Questo è vero specialmente nel caso degli emulatori. Poiché sono software,
anch'essi necessitano di manutenzione.
-
Mascherano gli errori. Ad esempio, supponiamo che il componente utilizzi delle funzioni trigonometriche ma non è
ancora disponibile una libreria. I tre scenari di test richiedono il seno di tre angoli: 10 gradi, 45 gradi e 90
gradi. Con la calcolatrice si individuano i valori corretti e si costruisce uno stub per il seno che restituisce
rispettivamente 0,173648178, 0,707106781 e 1.0. Funziona tutto correttamente finché non si integra il proprio
componente con la vera libreria trigonometrica, la cui funzione di seno accetta parametri espressi come
radianti e quindi restituisce -0,544021111, 0,850903525 e 0,893996664. Si tratta di un difetto nel codice
che viene rilevato in seguito, e con più impegno di quanto non si desideri.
A meno che gli stub non siano stati creati perché il vero componente non era ancora disponibile, si deve prevedere di
conservarli dopo lo sviluppo. I test che supportano probabilmente saranno importanti durante la manutenzione. Gli stub,
quindi, devono essere scritti con standard più elevati di quelli del codice da scarto. Anche se non devono rispettare
gli standard del codice del prodotto - ad esempio, la maggior parte non ha bisogno di una suite di test propria - in
seguito gli sviluppatori dovranno gestirli come componenti della modifica del prodotto. Se la manutenzione è tropo
difficile, gli stub verranno eliminati e il relativo investimento andrà perso.
Specialmente quando devono essere conservati, gli stub alterano la progettazione del componente. Ad esempio, si
supponga che il componente utilizzi un database per memorizzare permanentemente le coppie chiave/valore. Considerare
due scenari di progettazione:
Scenario 1: Il database viene utilizzato per la verifica oltre che per un uso normale. L'esistenza del database
non deve essere nascosto dal componente. È possibile inizializzarlo con il nome del database:
public Component(
String databaseURL) { try { databaseConnection = DriverManager.getConnection(databaseURL); ... } catch (SQLException e) {...} }
E, sebbene non si desideri che ogni posizione che ha letto o scritto un valore costruisca un'istruzione SQL, certamente si
avranno dei metodi che contengono SQL. Ad esempio, il codice del componente che ha bisogno di un valore chiama questo
metodo di componente:
public String get(String key) { try { Statement stmt = databaseConnection.createStatement(); ResultSet rs = stmt.executeQuery(
"SELECT value FROM Table1 WHERE key=" + key); ... } catch (SQLException e) {...} }
Scenario 2: Per la verifica, il database viene sostituito da uno stub. Il codice del componente dovrebbe essere lo
stesso a prescindere che sia in esecuzione sul vero database o sullo stub. Quindi deve essere codificato per utilizzare i
metodi di un'interfaccia astratta:
interface KeyValuePairs { String
get(String key); void
put(String key, String value); }
I test implementerebberot KeyValuePairs con qualcosa di semplice come una tabella di hash:
class FakeDatabase implementa KeyValuePairs { Hashtable table = new Hashtable(); public String
get(String key) { return (String) table.get(key); } public void
put(String key, String value) { table.put(key, value); } }
Quando non viene testato, il componente utilizza un oggetto adattatore che converte
le chiamate all'interfaccia KeyValuePairs in istruzioni SQL:
class DatabaseAdapter implementa KeyValuePairs { private Connection databaseConnection; public DatabaseAdapter(String databaseURL) { try { databaseConnection = DriverManager.getConnection(databaseURL); ... } catch (SQLException e) {...} } public String
get(String key) { try { Statement stmt = databaseConnection.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT value FROM Table1 WHERE key=" + key); ... } catch (SQLException e) {...} } public void
put(String key, String value) { ... } }
Il componente potrebbe avere un singolo costruttore per entrambi i test ed altri client. Il costruttore prenderebbe un
oggetto che implementa KeyValuePairs. Oppure potrebbe fornire quell'interfaccia solo per i test,
richiedendo che i client ordinari del componente passino il nome di un database:
class Component {
public Component(String databaseURL) { this.valueStash = new DatabaseAdapter(databaseURL); } // For testing.
protetto Component(KeyValuePairs valueStash) { this.valueStash = valueStash; } }
Quindi, dal punto di vista dei programmatori client, i due scenari di progettazione producono la stessa API, ma una è
testabile più prontamente. (Alcuni test possono utilizzare il vero database ed altri quello stub).
Per ulteriori informazioni correlate agli stub, consultare quanto segue:
|