2012-04-05 7 views
5

Sono un test di unità in una classe in cui ho bisogno di un certo periodo di tempo prima di poter verificare i risultati. In particolare ho bisogno di x minuti per passare prima di poter dire se il test ha funzionato o meno. Ho letto che nei test unitari dovremmo testare l'interfaccia e non l'implementazione, quindi non dovremmo accedere a variabili private, ma oltre a mettere un sleep nel mio test unitario non so come testare senza modificare le variabili private.Come testare il passare del tempo in jUnit test senza accedere a variabili private?

Il mio test è impostato in questo modo:

@Test 
public void testClearSession() { 
    final int timeout = 1; 
    final String sessionId = "test"; 
    sessionMgr.setTimeout(timeout); 
    try { 
     sessionMgr.createSession(sessionId); 
    } catch (Exception e) { 
     e.printStackTrace(); 
    } 
    DBSession session = sessionMgr.getSession(sessionId); 
    sessionMgr.clearSessions(); 
    assertNotNull(sessionMgr.getSession(sessionId)); 
    Calendar accessTime = Calendar.getInstance(); 
    accessTime.add(Calendar.MINUTE, - timeout - 1); 
    session.setAccessTime(accessTime.getTime()); // MODIFY PRIVATE VARIABLE VIA PROTECTED SETTER 
    sessionMgr.clearSessions(); 
    assertNull(sessionMgr.getSession(sessionId)); 
} 

E 'possibile testare questa diversa modificando la variabile privata accessTime (tramite la creazione del setter setAccessTime o riflessione), o l'inserimento di un sonno nella prova di unità ?

EDIT 11-Aprile-2012

Sono in particolare cercando di verificare che il mio oggetto SessionManager cancella le sessioni dopo un determinato periodo di tempo è passato. Il database a cui mi sto connettendo interromperà le connessioni dopo un determinato periodo di tempo. Quando mi avvicino a quel timeout, l'oggetto SessionManager cancellerà le sessioni chiamando una procedura di "finalizzazione della sessione" sul database e rimuovendo le sessioni dal suo elenco interno.

L'oggetto SessionManager è progettato per essere eseguito in un thread separato. Il codice che sto testando si presenta così:

public synchronized void clearSessions() { 
    log.debug("clearSessions()"); 
    Calendar cal = Calendar.getInstance(); 
    cal.add(Calendar.MINUTE, - timeout); 
    Iterator<Entry<String, DBSession>> entries = sessionList.entrySet().iterator(); 
    while (entries.hasNext()) { 
     Entry<String, DBSession> entry = entries.next(); 
     DBSession session = entry.getValue(); 
     if (session.getAccessTime().before(cal.getTime())) { 
      // close connection 
      try { 
       connMgr.closeconn(session.getConnection(), entry.getKey()); 
      } catch (Exception e) { 
       e.printStackTrace(); 
      } 
      entries.remove(); 
     } 
    } 
} 

La chiamata a connMgr (oggetto ConnectionManager) è un po 'contorto, ma sono in fase di refactoring codice legacy ed è quello che è in questo momento. L'oggetto Session memorizza una connessione al database e alcuni dati associati.

+0

Una nota a margine: i test di unità in genere non devono testare nulla che funzioni in background (in thread separati). Quindi puoi dividere la logica e eseguirla in un altro thread. Il primo potrebbe quindi essere facilmente testato con i test unitari e il secondo con i test funzionali/di accettazione – zerkms

+0

Il codice che stai testando chiama 'System.getCurrentTimeMillis()' (o qualcosa di equivalente come 'new Date()')? Se è così, questa è una sorta di anti-pattern di testabilità; vuoi delegare questo a una classe "sorgente temporale", che puoi iniettare. Quindi, puoi eseguire i test con una fonte temporale fittizia. Se puoi pubblicare la fonte che stai cercando di testare, posso aiutarti a capire come fare ciò che suggerisco. –

+0

@DavidWallace - Ho appena assunto che GetInstance sia quella chiamata. JavaDocs conferma che il metodo getInstance di Calendar restituisce un oggetto Calendario i cui campi orari sono stati inizializzati con la data e l'ora correnti: ' – Gishu

risposta

4
  • Il test poteva fare con alcuni refactoring per rendere l'intento più chiaro. Se quello che capisco è corretto ...

.

public void TestClearSessionsMaintainsSessionsUnlessLastAccessTimeIsOverThreshold() { 

    final int timeout = 1; 
    final String sessionId = "test"; 
    sessionMgr = GetSessionManagerWithTimeout(timeout); 
    DBSession session = CreateSession(sessionMgr, sessionId); 

    sessionMgr.clearSessions(); 
    assertNotNull(sessionMgr.getSession(sessionId)); 

    session.setAccessTime(PastInstantThatIsOverThreshold()); // MODIFY PRIVATE VARIABLE VIA PROTECTED SETTER 
    sessionMgr.clearSessions(); 
    assertNull(sessionMgr.getSession(sessionId)); 
} 
  • Ora per la questione dei test, senza dover esporre stato privato
    • Come è la variabile privata modificato nella vita reale? C'è qualche altro metodo pubblico che potresti chiamare quali aggiornamenti il ​​tempo di accesso?
    • Poiché l'ora/ora è un concetto importante, perché non renderlo esplicito come un ruolo. Quindi potresti passare un oggetto Clock alla Session, che usa per aggiornare il suo tempo di accesso interno. Nei tuoi test, potresti passare in un MockClock, il cui metodo getCurrentTime() restituirebbe qualsiasi valore tu desideri. Sto inventando la sintassi di derisione .. quindi aggiorna con qualunque cosa tu stia usando.

.

public void TestClearSessionsMaintainsSessionsUnlessLastAccessTimeIsOverThreshold() { 

     final int timeout = 1; 
     final String sessionId = "test"; 
     expect(mockClock).GetCurrentTime(); willReturn(CurrentTime()); 
     sessionMgr = GetSessionManagerWithTimeout(timeout, mockClock); 
     DBSession session = CreateSession(sessionMgr, sessionId); 

     sessionMgr.clearSessions(); 
     assertNotNull(sessionMgr.getSession(sessionId)); 

     expect(mockClock).GetCurrentTime(); willReturn(PastInstantThatIsOverThreshold()); 
     session.DoSomethingThatUpdatesAccessTime(); 
     sessionMgr.clearSessions(); 
     assertNull(sessionMgr.getSession(sessionId)); 
} 
+0

+1 per il nome del metodo di prova .. – Jayan

+0

Dovresti menzionare che hai usato [mockito] (http://code.google.com/p/mockito/) per ottenere quella bella sintassi di simulazione. –

+0

@ BjörnPollex sei sicuro? A me non sembra una sintassi mockito. –

1

EDIT: Mi piace la risposta di Gishu. Ti incoraggia anche a prendere in giro il tempo, ma lo tratta come un oggetto di prima classe.

Qual è esattamente la regola che stai tentando di testare? Se sto leggendo il tuo codice, sembra che il tuo desiderio sia verificare che la sessione associata all'ID "test" scada dopo un determinato timeout, corretto?

Il tempo è una cosa complicata nei test unitari perché è essenzialmente stato globale, quindi questo è un candidato migliore per un test di accettazione (come suggerito da zerkms).

Se si desidera eseguire un test dell'unità, in genere cerco di astrarre e/o isolare i riferimenti al tempo, in modo da poterli prendere in giro nei miei test. Un modo per farlo è sottoclassi della classe sotto test. Si tratta di una leggera interruzione nell'incapsulamento, ma funziona in modo più pulito rispetto a un metodo di setter protetto e molto meglio della riflessione.

Un esempio:

class MyClass { 
    public void doSomethingThatNeedsTime(int timeout) { 
    Date now = getNow(); 
    if (new Date().getTime() > now.getTime() + timeout) { 
     // timed out! 
    } 
    } 

    Date getNow() { 
    return new Date(); 
    } 
} 

class TestMyClass { 
    @Test 
    public void testDoSomethingThatNeedsTime() { 
    MyClass mc = new MyClass() { 
     Date getNow() { 
     // return a time appropriate for my test 
     }  
    }; 

    mc.doSomethingThatNeedsTime(1); 

    // assert 
    } 
} 

Questo è un po 'un esempio forzato, ma si spera che si ottiene il punto. Sottoclassi il metodo getNow(), il mio test non è più soggetto al tempo globale. Posso sostituire il tempo che voglio.

Come ho detto, questo rompe un po 'l'incapsulamento, perché il metodo getNow() REAL non viene mai testato e richiede che il test conosca qualcosa sull'implementazione. Ecco perché è bene mantenere un metodo così piccolo e mirato, senza effetti collaterali. Questo esempio presuppone anche che la classe sottoposta a test non sia definitiva.

Nonostante gli inconvenienti, è più pulito (a mio parere) che fornire un setter con scope per una variabile privata, che può effettivamente consentire a un programmatore di fare del male. Nel mio esempio, se alcuni processi anomali invocano il metodo getNow(), non c'è alcun danno reale.

+0

Sì, sto cercando di intercettare la sessione appena prima che la connessione al database associata scada, così posso chiudere la connessione in modo pulito, piuttosto che scadere. Grazie per i commenti, la diversa prospettiva è stata utile. – Allan5

1

Sembra che la funzionalità in fase di test sia SessionManager che evita tutte le sessioni scadute.

Vorrei prendere in considerazione la creazione di una classe di test che estenda DBSession.

AlwaysExpiredDBSession extends DBSession { 
.... 
// access time to be somewhere older 'NOW' 

} 
+0

Questa è una possibilità interessante sebbene i miei oggetti DBSession siano creati dall'oggetto SessionManager che sto testando, quindi avrei bisogno di creare un modo per iniettare l'oggetto. La mia attuale interfaccia consente la creazione e il recupero di DBSessions ma nessun inserimento. Penso che finirei per venire allo stesso problema di dover modificare una variabile privata sull'oggetto SessionManager piuttosto che una variabile privata dell'oggetto DBSession. – Allan5

+0

Ho pensato a questo e ho capito che avrebbe funzionato.Nel caso generale questo approccio funzionerebbe sicuramente, ma anche nel mio caso sto creando gli oggetti DBSession usando una factory, quindi ho solo bisogno di passare un factory diverso al SessionManager e dovrebbe funzionare. – Allan5

0

Io fondamentalmente seguito il suggerimento di Gishu https://stackoverflow.com/a/10023832/1258214, ma ho pensato di documentare i cambiamenti solo a beneficio di chiunque altro leggendo questo (e quindi chiunque può commentare problemi con l'implementazione). Grazie al commento mi sta indicando JodaTime e Mockito.

L'idea era di riconoscere la dipendenza del codice in tempo e di estrarlo (vedi: https://stackoverflow.com/a/5622222/1258214). Ciò è stato fatto con la creazione di un'interfaccia:

import org.joda.time.DateTime; 

public interface Clock { 
    public DateTime getCurrentDateTime() ; 
} 

quindi la creazione di un'implementazione:

import org.joda.time.DateTime; 

public class JodaClock implements Clock { 

    @Override 
    public DateTime getCurrentDateTime() { 
     return new DateTime(); 
    } 

} 

Questo è stato poi passato nel costruttore di SessionManager:

SessionManager(ConnectionManager connMgr, SessionGenerator sessionGen, 
     ObjectFactory factory, Clock clock) { 

sono stato poi in grado di utilizzare un codice simile a quanto suggerito da Gishu (notare la lettera "t" minuscola all'inizio del testClear ... i miei test unitari hanno avuto molto successo con la lettera maiuscola "T" fino a quando ho realizzato che il test non era in esecuzione ...):

@Test 
public void testClearSessionsMaintainsSessionsUnlessLastAccessTimeIsOverThreshold() { 
    final String sessionId = "test"; 
    final Clock mockClock = mock(Clock.class); 

    when(mockClock.getCurrentDateTime()).thenReturn(getNow()); 
    SessionManager sessionMgr = getSessionManager(connMgr, 
      sessionGen, factory, mockClock); 
    createSession(sessionMgr, sessionId); 

    sessionMgr.clearSessions(defaultTimeout); 
    assertNotNull(sessionMgr.getSession(sessionId)); 

    when(mockClock.getCurrentDateTime()).thenReturn(getExpired()); 

    sessionMgr.clearSessions(defaultTimeout); 
    assertNull(sessionMgr.getSession(sessionId)); 
} 

Questo ha funzionato grande, ma la mia rimozione del Session.setAccessTime() ha creato un problema con un altro test testOnlyExpiredSessionsCleared() dove volevo una sessione per scadere, ma non l'altro. Questo collegamento https://stackoverflow.com/a/6060814/1258214 mi ha portato a pensare al design del SessionManager.metodo clearSessions() e ho refactored il controllo se una sessione è scaduta da SessionManager, all'oggetto DBSession stesso.

Da:

if (session.getAccessTime().before(cal.getTime())) { 

A:

if (session.isExpired(expireTime)) { 

Poi ho inserito un oggetto mockSession (simile al suggerimento di Jayan https://stackoverflow.com/a/10023916/1258214)

@Test 
public void testOnlyOldSessionsCleared() { 
    final String sessionId = "test"; 
    final String sessionId2 = "test2"; 

    ObjectFactory mockFactory = spy(factory); 
    SessionManager sm = factory.createSessionManager(connMgr, sessionGen, 
     mockFactory, clock); 

    // create expired session 
    NPIISession session = factory.createNPIISession(null, clock); 
    NPIISession mockSession = spy(session); 
    // return session expired 
    doReturn(true).when(mockSession).isExpired((DateTime) anyObject()); 

    // get factory to return mockSession to sessionManager 
    doReturn(mockSession).when(mockFactory).createDBSession(
     (Connection) anyObject(), eq(clock)); 
    createSession(sm, sessionId); 

    // reset factory so return normal session 
    reset(mockFactory); 
    createSession(sm, sessionId2); 

    assertNotNull(sm.getSession(sessionId)); 
    assertNotNull(sm.getSession(sessionId2)); 

    sm.clearSessions(defaultTimeout); 
    assertNull(sm.getSession(sessionId)); 
    assertNotNull(sm.getSession(sessionId2)); 
} 

Grazie a tutti per il loro aiuto con questo . Per favore fatemi sapere se vedete eventuali problemi con le modifiche.

Problemi correlati