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

62

Stiamo provando a progettare il nostro sistema affinché sia testabile e nella maggior parte delle parti sviluppato utilizzando TDD. Attualmente stiamo cercando di risolvere il seguente problema:

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

Ho diverse idee per risolvere questo problema:

  1. Utilizzare un framework di simulazione che possa prendere in giro classi statiche (come PowerMock). Questa potrebbe essere la soluzione più semplice, ma in qualche modo è come rinunciare.
  2. Crea classi wrapper istanziabili attorno a tutte quelle utility statiche in modo che possano essere iniettate nelle classi che li utilizzano. Sembra una soluzione relativamente pulita, ma temo che finiremo per creare un sacco di quelle classi wrapper.
  3. Estrai ogni chiamata a queste classi di helper statiche in una funzione che può essere sovrascritta e testare una sottoclasse della classe che in realtà voglio testare.

Ma continuo a pensare che questo deve essere 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 le classi che utilizzano questi helper statici testabili?

    
posta Benedikt 27.04.2012 - 10:48
fonte

7 risposte

33

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

Quando questi metodi statici rappresentano le dipendenze genuine , crea i 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 aiuta l'integrazione delle dipendenze)

... ha senso creare un'interfaccia.

Ma molti dei metodi in Apache Commons probabilmente non dovrebbero essere derisi / falsificati. Ad esempio, prendi un metodo per unire un elenco di stringhe, aggiungendo una virgola tra di esse. C'è un nessun punto nel prendere in giro questi - basta lasciare che la chiamata statica faccia il suo normale lavoro. Non vuoi o devi sostituire il normale comportamento; non hai a che fare con una risorsa esterna o qualcosa con cui è difficile lavorare, sono solo dati. Il risultato è prevedibile e non vorrai mai che sia altro di quello che ti darà comunque.

Sospetto di aver rimosso tutte le chiamate statiche che sono in realtà convenienza metodi con risultati prevedibili, "puri" (come base64 o codifica URL) piuttosto che punti di ingresso in un intero grande pasticcio di dipendenze logiche (come HTTP) troverai che è del tutto pratico fare la cosa giusta con le dipendenze genuine.

    
risposta data 09.05.2012 - 08:06
fonte
20

Questa è sicuramente una domanda / risposta supponente, ma per quello che vale ho pensato di buttare i miei due centesimi. In termini di stile TDD, il metodo 2 è sicuramente l'approccio che segue alla lettera. L'argomento del metodo 2 è che se hai mai voluto sostituire l'implementazione di una di queste classi, ad esempio una libreria equivalente a ImageIO , puoi farlo mantenendo la sicurezza nelle classi che sfruttano quel codice.

Tuttavia, come hai detto tu, se usi molti metodi statici, finirai per scrivere un sacco di codice wrapper. Questo potrebbe non essere una brutta cosa a lungo termine. In termini di manutenibilità ci sono certamente argomenti a riguardo. Personalmente preferirei questo approccio.

Detto questo, PowerMock esiste per una ragione. È un problema abbastanza noto che testare quando sono coinvolti metodi statici è seriamente doloroso, da qui la nascita di PowerMock. Penso che sia necessario valutare le opzioni in termini di quanto lavoro sarà necessario per completare tutte le classi di helper rispetto a PowerMock. Non penso che stia "rinunciando" all'utilizzo di PowerMock - sento solo che il wrapping delle classi ti consente maggiore flessibilità in un grande progetto. Più contratti pubblici (interfacce) puoi fornire all'aspirante la separazione tra intento e implementazione.

    
risposta data 27.04.2012 - 10:57
fonte
4

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

Seguiamo fondamentalmente il percorso delineato 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 prendere in giro 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 i mock per i metodi statici (se pensiamo che potremmo c'è bisogno di tempo per scrivere una classe wrapper e quindi prendere in giro un'istanza del wrapper).

    
risposta data 08.05.2012 - 11:02
fonte
2

Vorrei testare queste classi usando Groovy . Groovy è semplice da aggiungere a qualsiasi progetto Java. Con esso, puoi prendere facilmente in giro i metodi statici. Vedi Mocking Static Methods using Groovy per un esempio.

    
risposta data 03.05.2012 - 21:58
fonte
1

Lavoro per una grande compagnia di assicurazioni e il nostro codice sorgente arriva 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 di junit per ogni singolo componente.

La migliore soluzione del nostro dipartimento era usare gli oggetti Mock su alcuni metodi JNI che erano affidabili dal sistema (scritti in C) e in quanto tali non si potevano stimare esattamente i risultati ogni volta su ogni sistema operativo. Non avevamo altra scelta che usare classi mocked 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 - link

    
risposta data 08.05.2012 - 13:09
fonte
1

Gli oggetti interagiscono l'uno con l'altro per raggiungere un obiettivo, quando l'oggetto è difficile da testare a causa dell'ambiente (un endpoint webserver, il livello dao accede al DB, i controller gestiscono i parametri della richiesta http) o si desidera testare il proprio oggetto isolamento, quindi prendi in giro quegli oggetti.

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

    
risposta data 08.05.2012 - 13:33
fonte
1

A volte uso l'opzione 4

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

Qualcosa di simile.

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 i metodi di utilità statici, il che mi sembra giusto quando cerco di usare la classe per tutto il codice.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
    
risposta data 22.05.2015 - 18:46
fonte

Leggi altre domande sui tag