Sto lavorando a un progetto in cui dobbiamo implementare e testare le unità qualche nuovo modulo. Avevo in mente un'architettura abbastanza chiara, quindi ho rapidamente annotato la parte principale classi e metodi e poi abbiamo iniziato a scrivere test unitari.
Durante la scrittura dei test abbiamo dovuto apportare alcune modifiche a il codice originale, come
- Rendere pubblici i metodi privati per testarli
- Aggiunta di metodi aggiuntivi per accedere alle variabili private
- Aggiunta di metodi aggiuntivi per iniettare oggetti fittizi che dovrebbero essere utilizzati quando il codice viene eseguito all'interno di un test unitario.
In qualche modo ho la sensazione che questi siano sintomi che stiamo facendo qualcosa di sbagliato, per esempio
- il progetto iniziale era sbagliato (alcune funzionalità dovrebbero essere state pubbliche dall'inizio),
- il codice non è stato progettato correttamente per essere interfacciato con i test di unità (forse perché abbiamo iniziato a progettare i test di unità quando già alcune classi erano state progettate),
- stiamo implementando i test unitari nel modo sbagliato (ad esempio, i test unitari dovrebbero solo testare / indirizzare direttamente i metodi pubblici di un'API, non quelli privati),
- una combinazione dei tre punti precedenti e forse alcuni problemi aggiuntivi a cui non ho pensato.
Dato che ho una certa esperienza con i test unitari ma sono lontano dall'essere un guru, sarei molto interessato a leggere i tuoi pensieri riguardo a questi problemi.
Oltre alle domande generali di cui sopra, ho alcune più specifiche, domande tecniche:
Domanda 1. Ha senso testare direttamente un metodo privato m di una classe A e persino renderlo pubblico per testarlo? O dovrei supporre che m sia indirettamente testato dai test unitari che coprono altri metodi pubblici che chiamano m?
Domanda 2. Se un'istanza della classe A contiene un'istanza di classe B (aggregazione composita), ha senso prendere in giro B per testare A? La mia prima idea era che non dovevo prendere in giro B perché l'istanza B faceva parte dell'istanza A, ma poi ho iniziato a dubitarne. Il mio argomento contro il beffardo B è lo stesso di 1: B è privato wrt A e usato solo per la sua implementazione, quindi beffardo B sembra come se io esponessi dettagli privati di A like in (1). Ma forse questi problemi indicano un difetto di progettazione: forse non dovremmo usare l'aggregazione composita ma una semplice associazione da A a B.
Domanda 3. Nell'esempio precedente, se decidiamo di prendere in giro B, come facciamo ad iniettare l'istanza B in A? Ecco alcune idee che abbiamo:
- Inietti l'istanza B come argomento al costruttore A invece di creare l'istanza B nel costruttore A.
- Passa un'interfaccia BFactory come argomento al costruttore A e lascia A utilizzare la factory per creare l'istanza B privata.
- Utilizzare un singleton BFactory privato in A. Utilizzare un metodo statico A :: setBFactory () per impostare il singleton. Quando A vuole creare l'istanza B usa il singleton di fabbrica se è impostato (lo scenario di test), crea B direttamente se il singleton non è impostato (lo scenario del codice di produzione).
Le prime due alternative mi sembrano più pulite, ma richiedono un cambiamento la firma del costruttore A: cambiare l'API solo per renderlo più testabile mi sembra imbarazzante, è una pratica comune?
Il terzo ha il vantaggio che non richiede la modifica della firma del costruttore (la modifica all'API è meno invasiva), ma richiede il richiamo del metodo statico setBFactory () prima di iniziare il test, che è l'errore IMO -prone (dipendenza implicita da una chiamata di metodo affinché i test funzionino correttamente). Quindi non so quale scegliere.