Come gestire le classi di utilità statiche durante la progettazione per la testabilità


63

Stiamo cercando di progettare il nostro sistema in modo che sia testabile e nella maggior parte dei casi sviluppato utilizzando TDD. Attualmente stiamo cercando di risolvere il seguente problema:

In vari luoghi è necessario per noi utilizzare metodi di supporto statici come ImageIO e URLEncoder (entrambe le API Java standard) e varie altre librerie che consistono principalmente di metodi statici (come le librerie Apache Commons). Ma è estremamente difficile testare quei metodi che usano tali classi di supporto statiche.

Ho diverse idee per risolvere questo problema:

  1. Utilizzare un framework simulato in grado di deridere classi statiche (come PowerMock). Questa potrebbe essere la soluzione più semplice ma in qualche modo sembra di rinunciare.
  2. Crea classi wrapper istantanee attorno a tutte quelle utility statiche in modo che possano essere iniettate nelle classi che le usano. Sembra una soluzione relativamente pulita ma temo che finiremo per creare moltissime classi di wrapper.
  3. Estrai ogni chiamata a queste classi di supporto statiche in una funzione che può essere ignorata e testa una sottoclasse della classe che voglio testare.

Ma continuo a pensare che questo debba essere solo un problema che molte persone devono affrontare quando fanno TDD - quindi ci devono essere già soluzioni per questo problema.

Qual è la migliore strategia per mantenere testabili le classi che usano questi helper statici?


Non sono sicuro di cosa intendi per "fonti credibili e / o ufficiali", ma sono d'accordo con ciò che @berecursive ha scritto nella sua risposta. PowerMock esiste per una ragione e non dovrebbe sentirsi come "arrendersi" soprattutto se non si desidera scrivere da soli le classi wrapper. I metodi finali e statici sono un dolore quando si tratta di test unitari (e TDD). Personalmente? Uso il metodo 2 che hai descritto.
Deco

"fonti credibili e / o ufficiali" è solo una delle opzioni che è possibile selezionare quando si avvia una taglia per una domanda. Cosa intendo realmente: esperienze o riferimenti ad articoli scritti da esperti di TDD. O qualsiasi tipo di esperienza da parte di qualcuno che ha affrontato lo stesso problema ...

Risposte:


34

(Non ho fonti "ufficiali" qui, temo - non è che ci sia una specifica su come testare bene. Solo le mie opinioni, che si spera siano utili.)

Quando questi metodi statici rappresentano dipendenze autentiche , creare wrapper. Quindi per cose come:

  • ImageIO
  • Client HTTP (o qualsiasi altra cosa relativa alla rete)
  • Il file system
  • Ottenere l'ora corrente (il mio esempio preferito di dove l'iniezione di dipendenza aiuta)

... ha senso creare un'interfaccia.

Ma molti dei metodi di Apache Commons probabilmente non dovrebbero essere derisi / falsificati. Ad esempio, prendi un metodo per unire un elenco di stringhe, aggiungendo una virgola tra di loro. Non ha senso deriderli: lascia che la chiamata statica faccia il suo normale lavoro. Non si desidera o non è necessario sostituire il comportamento normale; non hai a che fare con una risorsa esterna o qualcosa con cui è difficile lavorare, sono solo dati. Il risultato è prevedibile e non vorresti mai che fosse qualcosa di diverso da quello che ti darà comunque.

Ho il sospetto che dopo aver rimosso tutte le chiamate statiche che sono in realtà metodi di convenienza con risultati prevedibili e "puri" (come la codifica base64 o URL) anziché i punti di ingresso in un gran casino di dipendenze logiche (come HTTP) scoprirai che è interamente pratico fare la cosa giusta con le dipendenze genuine.


20

Questa è sicuramente una domanda / risposta supponente ma per quello che vale la pena ho pensato di buttare i miei due centesimi. In termini di stile TDD il metodo 2 è sicuramente l'approccio che lo segue alla lettera. L'argomento per il metodo 2 è che se mai volessi sostituire l'implementazione di una di quelle classi - diciamo una ImageIOlibreria equivalente - allora potresti farlo mantenendo la fiducia nelle classi che sfruttano quel codice.

Tuttavia, come hai accennato, se usi molti metodi statici, finirai per scrivere molto codice wrapper. A lungo termine potrebbe non essere una brutta cosa. In termini di manutenibilità ci sono certamente argomenti per questo. Personalmente preferirei questo approccio.

Detto questo, PowerMock esiste per un motivo. È un problema abbastanza noto che testare quando sono coinvolti metodi statici è gravemente doloroso, da cui la nascita di PowerMock. Penso che tu debba valutare le tue opzioni in termini di quanto lavoro sarà per avvolgere tutte le tue classi di supporto rispetto a utilizzare PowerMock. Non credo che si debba rinunciare all'utilizzo di PowerMock: penso solo che il wrapping delle classi ti permetta una maggiore flessibilità in un grande progetto. Più appalti pubblici (interfacce) è possibile fornire più pulito la separazione tra intento e implementazione.


1
Un ulteriore problema di cui non sono molto sicuro: durante l'implementazione dei wrapper implementereste tutti i metodi della classe che è racchiusa o solo quelli attualmente necessari?

3
Nel seguire idee agili, dovresti fare la cosa più semplice che funziona ed evitare di fare un lavoro che non ti serve. Pertanto, è necessario esporre solo i metodi effettivamente necessari.
Assaf Stone,

@AssafStone concordato

Fai attenzione con PowerMock, tutta la manipolazione della classe che deve fare per deridere i metodi comporta un sacco di spese generali. I tuoi test saranno molto più lenti se lo usi ampiamente.
bcarlso,

Devi davvero scrivere molto wrapper se associ i tuoi test / migrazione all'adozione di una libreria DI / IoC?

4

Come riferimento per tutti coloro che hanno a che fare con questo problema e che incontrerò questa domanda, descriverò come abbiamo deciso di affrontare il problema:

Fondamentalmente stiamo seguendo il percorso indicato come # 2 (classi wrapper per utility statiche). Ma li usiamo solo quando è troppo complesso per fornire all'utilità i dati richiesti per produrre l'output desiderato (cioè quando dobbiamo assolutamente deridere il metodo).

Questo significa che non dobbiamo scrivere un wrapper per una semplice utility come Apache Commons StringEscapeUtils(perché le stringhe di cui hanno bisogno possono essere facilmente fornite) e non usiamo le beffe per metodi statici (se pensiamo che potremmo aver bisogno di farlo è il momento di scrivere una classe wrapper e quindi deridere un'istanza del wrapper).



1

Lavoro per una grande compagnia assicurativa e il nostro codice sorgente arriva fino a 400 MB di file java puri. Abbiamo sviluppato l'intera applicazione senza pensare a TDD. Da gennaio di quest'anno abbiamo iniziato con i test junit per ogni singolo componente.

La migliore soluzione nel nostro dipartimento era quella di utilizzare oggetti Mock su alcuni metodi JNI che erano affidabili per il sistema (scritti in C) e come tali non si poteva esattamente stimare i risultati ogni volta su ogni SO. Non avevamo altra scelta che usare classi derise e implementazioni specifiche dei metodi JNI specificamente allo scopo di testare ogni singolo modulo dell'applicazione per ogni sistema operativo che supportiamo.

Ma è stato davvero veloce e finora ha funzionato abbastanza bene. Lo consiglio - http://www.easymock.org/


1

Gli oggetti interagiscono tra loro per raggiungere un obiettivo, quando si ha un oggetto difficile da testare a causa dell'ambiente (un endpoint del servizio web, strato dao che accede al DB, controller che gestiscono i parametri di richiesta http) o si desidera testare il proprio oggetto in isolamento, quindi prendi in giro quegli oggetti.

la necessità di deridere i metodi statici è un cattivo odore, devi progettare la tua applicazione più orientata agli oggetti e i metodi statici dell'utility unit testing non aggiungono molto valore al progetto, la classe wrapper è un buon approccio a seconda della situazione, ma prova per testare quegli oggetti che usano i metodi statici.


1

A volte uso l'opzione 4

  1. Usa il modello di strategia. Creare una classe di utilità con metodi statici che delegano l'implementazione a un'istanza di interfaccia collegabile. Codifica un inizializzatore statico che collega un'implementazione concreta. Collegare un'implementazione fittizia per i test.

Qualcosa come questo.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

Quello che mi piace di questo approccio è che mantiene statici i metodi di utilità, il che è giusto per me quando sto cercando di usare la classe in tutto il codice.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.