In che modo i test unitari dovrebbero impostare origini dati quando non sono in esecuzione in un server applicazioni?

StackOverflow https://stackoverflow.com/questions/831760

Domanda

Grazie a tutti per il vostro aiuto. Alcune di voi hanno pubblicato (come avrei dovuto aspettarmi) risposte indicando che il mio intero approccio era sbagliato o che quel codice di basso livello non dovrebbe mai sapere se è in esecuzione in un contenitore. Tenderei ad essere d'accordo. Tuttavia, ho a che fare con un'applicazione legacy complessa e non ho la possibilità di effettuare un refactoring importante per il problema attuale.

Vorrei fare un passo indietro e porre la domanda motivata alla mia domanda originale.

Ho un'applicazione legacy in esecuzione su JBoss e ho apportato alcune modifiche al codice di livello inferiore. Ho creato un test unitario per la mia modifica. Per eseguire il test, devo collegarmi a un database.

Il codice legacy ottiene l'origine dati in questo modo:

(jndiName è una stringa definita)

Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);

Il mio problema è che quando eseguo questo codice in unit test, il contesto non ha origini dati definite. La mia soluzione a questo è stata quella di provare a vedere se sono in esecuzione sotto il server delle applicazioni e, in caso contrario, creare l'origine dati di test e restituirlo. Se corro sotto il server delle app, utilizzo il codice sopra.

Quindi, la mia vera domanda è: qual è il modo corretto per farlo? Esiste un modo approvato in cui il test unitario può impostare il contesto per restituire l'origine dati appropriata in modo che il codice sottoposto a test non debba essere consapevole di dove è in esecuzione?


Per il contesto: LA MIA DOMANDA ORIGINALE:

Ho del codice Java che deve sapere se funziona o meno con JBoss. Esiste un modo canonico per il codice per dire se è in esecuzione in un contenitore?

Il mio primo approccio è stato sviluppato attraverso la sperimentazione e consiste nell'ottenere il contesto iniziale e testare che può cercare determinati valori.

private boolean isRunningUnderJBoss(Context ctx) {
        boolean runningUnderJBoss = false;
        try {
            // The following invokes a naming exception when not running under
            // JBoss.
            ctx.getNameInNamespace();

            // The URL packages must contain the string "jboss".
            String urlPackages = (String) ctx.lookup("java.naming.factory.url.pkgs");
            if ((urlPackages != null) && (urlPackages.toUpperCase().contains("JBOSS"))) {
                runningUnderJBoss = true;
            }
        } catch (Exception e) {
            // If we get there, we are not under JBoss
            runningUnderJBoss = false;
        }
        return runningUnderJBoss;
    }

Context ctx = new InitialContext();
if (isRunningUnderJboss(ctx)
{
.........

Ora, sembra funzionare, ma sembra un trucco. Qual è il "corretto" modo di fare questo? Idealmente, mi piacerebbe un modo che funzionasse con una varietà di server delle applicazioni, non solo con JBoss.

È stato utile?

Soluzione

L'intero approccio mi sembra sbagliato. Se la tua app deve sapere in quale contenitore è in esecuzione, stai facendo qualcosa di sbagliato.

Quando uso Spring posso spostarmi da Tomcat a WebLogic e viceversa senza cambiare nulla. Sono sicuro che con una corretta configurazione potrei fare lo stesso trucco anche con JBOSS. Questo è l'obiettivo per cui avrei sparato.

Altri suggerimenti

L'intero concetto è in primo piano. Il codice di livello inferiore non dovrebbe eseguire questo tipo di test. Se hai bisogno di un'implementazione diversa, passala in un punto pertinente.

Qualche combinazione di Dependency Injection (tramite Spring, file di configurazione o argomenti del programma) e Factory Pattern di solito funzionerebbe meglio.

Come esempio passo un argomento ai miei script Ant che impostano i file di configurazione a seconda che l'orecchio o la guerra vadano in un ambiente di sviluppo, test o produzione.

Forse qualcosa del genere (brutto ma potrebbe funzionare)

 private void isRunningOn( String thatServerName ) { 

     String uniqueClassName = getSpecialClassNameFor( thatServerName );
     try { 
         Class.forName( uniqueClassName );
     } catch ( ClassNotFoudException cnfe ) { 
         return false;
     }
     return true;
 } 

Il metodo getSpecialClassNameFor restituisce una classe univoca per ciascun server applicazioni (e può restituire nuovi nomi di classe quando vengono aggiunti più server di app)

Quindi lo usi come:

  if( isRunningOn("JBoss")) {
         createJBossStrategy....etcetc
  }
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);

Chi costruisce InitialContext? La sua costruzione deve essere al di fuori del codice che stai cercando di testare, altrimenti non sarai in grado di deridere il contesto.

Dato che hai detto che stai lavorando su un'applicazione legacy, per prima cosa refactoring il codice in modo da poter facilmente inserire la dipendenza nel contesto o nell'origine dati nella classe. Quindi puoi scrivere più facilmente i test per quella classe.

È possibile eseguire la transizione del codice legacy disponendo di due costruttori, come nel codice seguente, fino a quando non è stato eseguito il refactoring del codice che costruisce la classe. In questo modo puoi testare Foo più facilmente e mantenere invariato il codice che utilizza Foo. Quindi puoi refactificare lentamente il codice, in modo che il vecchio costruttore venga completamente rimosso e tutte le dipendenze vengano iniettate.

public class Foo {
  private final DataSource dataSource;
  public Foo() { // production code calls this - no changes needed to callers
    Context ctx = new InitialContext();
    this.dataSource = (DataSource) ctx.lookup(jndiName);
  }
  public Foo(DataSource dataSource) { // test code calls this
    this.dataSource = dataSource;
  }
  // methods that use dataSource
}

Ma prima di iniziare a fare quel refactoring, dovresti fare alcuni test di integrazione per coprirti la schiena. Altrimenti non puoi sapere se anche i semplici refactoring, come spostare la ricerca DataSource nel costruttore, rompono qualcosa. Quindi, quando il codice diventa migliore, più testabile, è possibile scrivere test unitari. (Per definizione, se un test tocca il file system, la rete o il database, non è un test unitario - è un test di integrazione.)

Il vantaggio dei test unitari è che corrono veloci - centinaia o migliaia al secondo - e sono molto focalizzati sul test di un solo comportamento alla volta. Ciò rende possibile l'esecuzione quindi spesso (se esiti a eseguire tutti i test unitari dopo aver cambiato una riga, vengono eseguiti troppo lentamente) in modo da ottenere un feedback rapido. E poiché sono molto focalizzati, saprai solo guardando il nome del test fallito che esattamente dove si trova il codice di produzione nel bug

Il vantaggio dei test di integrazione è che si assicurano che tutte le parti siano collegate correttamente. Anche questo è importante, ma non puoi eseguirli molto spesso perché cose come toccare il database li rendono molto lenti. Ma dovresti comunque eseguirli almeno una volta al giorno sul tuo server di integrazione continua.

Esistono un paio di modi per affrontare questo problema. Uno è passare un oggetto Context alla classe quando è sotto unit test. Se non è possibile modificare la firma del metodo, refactoring la creazione del contesto iniziale in un metodo protetto e testare una sottoclasse che restituisce l'oggetto contestuale deriso sovrascrivendo il metodo. Questo può almeno mettere alla prova la classe in modo da poter fare il refactoring per alternative migliori da lì.

L'opzione successiva è rendere le connessioni al database una fabbrica in grado di dire se si trova in un contenitore o meno, e fare la cosa appropriata in ogni caso.

Una cosa a cui pensare è: una volta che hai questa connessione al database fuori dal contenitore, cosa ne farai? È più semplice, ma non è un vero test unitario se devi trasportare l'intero livello di accesso ai dati.

Per ulteriore aiuto in questa direzione di spostamento del codice legacy in unit test, ti suggerisco di guardare Lavorare efficacemente con il codice legacy .

Un modo chiaro per farlo sarebbe quello di avere i listener del ciclo di vita configurati in web.xml . Questi possono impostare flag globali se lo desideri. Ad esempio, puoi definire un ServletContextListener nel tuo web.xml e nel metodo contextInitialized , imposta un flag globale che stai eseguendo all'interno di un contenitore. Se il flag globale non è impostato, non si esegue all'interno di un contenitore.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top