Best practice: inizializzare i campi della classe JUnit in setUp () o alla dichiarazione?


120

Devo inizializzare i campi della classe in una dichiarazione come questa?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

O in setUp () come questo?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Tendo a utilizzare il primo modulo perché è più conciso e mi consente di utilizzare i campi finali. Se non ho bisogno di usare il metodo setUp () per la configurazione, dovrei comunque usarlo, e perché?

Chiarimento: JUnit creerà un'istanza della classe di test una volta per metodo di test. Ciò significa listche verrà creato una volta per test, indipendentemente da dove lo dichiaro. Significa anche che non ci sono dipendenze temporali tra i test. Quindi sembra che non ci siano vantaggi nell'usare setUp (). Tuttavia le FAQ di JUnit hanno molti esempi che inizializzano una raccolta vuota in setUp (), quindi immagino che ci debba essere un motivo.


2
Attenzione che la risposta è diversa in JUnit 4 (inizializza nella dichiarazione) e JUnit 3 (usa setUp); questa è la radice della confusione.
Nils von Barth

Risposte:


99

Se ti stai chiedendo specificamente sugli esempi nelle FAQ di JUnit, come il modello di test di base , penso che la migliore pratica mostrata lì sia che la classe sotto test dovrebbe essere istanziata nel tuo metodo setUp (o in un metodo di test) .

Quando gli esempi JUnit creano un ArrayList nel metodo setUp, tutti continuano a testare il comportamento di quell'ArrayList, con casi come testIndexOutOfBoundException, testEmptyCollection e simili. La prospettiva che c'è di qualcuno che scrive una lezione e si assicura che funzioni correttamente.

Probabilmente dovresti fare lo stesso quando provi le tue classi: crea il tuo oggetto in setUp o in un metodo di test, in modo da essere in grado di ottenere un output ragionevole se lo interrompi in seguito.

D'altra parte, se usi una classe di raccolta Java (o un'altra classe di libreria, se è per questo) nel tuo codice di test, probabilmente non è perché vuoi testarlo - è solo una parte del dispositivo di test. In questo caso, puoi tranquillamente presumere che funzioni come previsto, quindi inizializzarlo nella dichiarazione non sarà un problema.

Per quel che vale, lavoro su una base di codice sviluppata da TDD abbastanza grande, vecchia di diversi anni. Inizializziamo abitualmente le cose nelle loro dichiarazioni in codice di test, e nell'anno e mezzo in cui sono stato su questo progetto, non ha mai causato problemi. Quindi ci sono almeno alcune prove aneddotiche che è una cosa ragionevole da fare.


45

Ho iniziato a scavare da solo e ho trovato un potenziale vantaggio nell'utilizzo setUp(). Se vengono generate eccezioni durante l'esecuzione disetUp() , JUnit stamperà una traccia dello stack molto utile. D'altra parte, se viene generata un'eccezione durante la costruzione dell'oggetto, il messaggio di errore dice semplicemente che JUnit non è stato in grado di istanziare il test case e non vedi il numero di riga in cui si è verificato l'errore, probabilmente perché JUnit utilizza la riflessione per istanziare il test classi.

Niente di tutto ciò si applica all'esempio della creazione di una raccolta vuota, poiché non verrà mai generata, ma è un vantaggio del setUp()metodo.


18

Oltre alla risposta di Alex B.

È anche necessario utilizzare il metodo setUp per istanziare le risorse in un determinato stato. Fare ciò nel costruttore non è solo una questione di tempi, ma a causa del modo in cui JUnit esegue i test, ogni stato del test verrebbe cancellato dopo averne eseguito uno.

JUnit crea prima istanze di testClass per ogni metodo di test e avvia l'esecuzione dei test dopo la creazione di ciascuna istanza. Prima di eseguire il metodo di prova, viene eseguito il suo metodo di configurazione, in cui è possibile preparare uno stato.

Se lo stato del database venisse creato nel costruttore, tutte le istanze creerebbero un'istanza dello stato del database una dopo l'altra, prima di eseguire ogni test. A partire dal secondo test, i test verrebbero eseguiti con uno stato sporco.

Ciclo di vita di JUnits:

  1. Crea un'istanza di testclass diversa per ogni metodo di test
  2. Ripeti per ogni istanza di testclass: chiama setup + chiama il metodo di test

Con alcune registrazioni in un test con due metodi di test ottieni: (il numero è il codice hash)

  • Creazione di una nuova istanza: 5718203
  • Creazione di una nuova istanza: 5947506
  • Configurazione: 5718203
  • TestOne: 5718203
  • Configurazione: 5947506
  • TestTwo: 5947506

3
Corretto, ma fuori tema. Il database è essenzialmente uno stato globale. Questo non è un problema che devo affrontare. Mi interessa solo la velocità di esecuzione di test adeguatamente indipendenti.
Craig P. Motlin,

Questo ordine di inizializzazione è vero solo in JUnit 3, dove è un avvertimento importante. In JUnit 4 istanze di test vengono create pigramente, quindi l'inizializzazione nella dichiarazione o in un metodo di configurazione avvengono entrambe in fase di test. Anche per la configurazione una tantum, si può usare @BeforeClassin JUnit 4.
Nils von Barth

11

In JUnit 4:

  • Per la classe sotto test , inizializzare in un @Beforemetodo per rilevare gli errori.
  • Per altre classi , inizializza nella dichiarazione ...
    • ... per brevità e per contrassegnare i campi final, esattamente come indicato nella domanda,
    • ... a meno che non si tratti di un'inizializzazione complessa che potrebbe non riuscire, nel qual caso utilizzare @Before, per rilevare gli errori.
  • Per lo stato globale (specialmente inizializzazione lenta , come un database), usa @BeforeClass, ma fai attenzione alle dipendenze tra i test.
  • L'inizializzazione di un oggetto utilizzato in un singolo test dovrebbe ovviamente essere eseguita nel metodo di test stesso.

L'inizializzazione in un @Beforemetodo o in un metodo di test consente di ottenere una migliore segnalazione degli errori sui guasti. Ciò è particolarmente utile per istanziare la classe sotto test (che potresti interrompere), ma è anche utile per chiamare sistemi esterni, come l'accesso al filesystem ("file non trovato") o la connessione a un database ("connessione rifiutata").

È accettabile avere uno standard semplice e utilizzarlo sempre@Before (errori chiari ma verboso) o inizializzarlo sempre in dichiarazione (conciso ma dà errori confusi), poiché le regole di codifica complesse sono difficili da seguire e questo non è un grosso problema.

L'inizializzazione in setUpè una reliquia di JUnit 3, in cui tutte le istanze di test sono state inizializzate con entusiasmo, il che causa problemi (velocità, memoria, esaurimento delle risorse) se si esegue un'inizializzazione costosa. Pertanto, la migliore pratica consisteva nell'inizializzazione costosa in setUp, che veniva eseguita solo quando veniva eseguito il test. Questo non si applica più, quindi è molto meno necessario da usare setUp.

Questo riassume molte altre risposte che seppelliscono la lede, in particolare da Craig P. Motlin (domanda stessa e auto-risposta), Moss Collum (classe in esame) e dsaff.


7

In JUnit 3, gli inizializzatori di campo verranno eseguiti una volta per metodo di test prima di eseguire qualsiasi test . Finché i valori dei campi sono piccoli nella memoria, richiedono poco tempo di configurazione e non influenzano lo stato globale, l'uso degli inizializzatori di campo è tecnicamente corretto. Tuttavia, se questi non sono validi, potresti finire per consumare molta memoria o tempo per impostare i tuoi campi prima che venga eseguito il primo test, e forse anche per esaurire la memoria. Per questo motivo, molti sviluppatori impostano sempre i valori dei campi nel metodo setUp (), dove è sempre sicuro, anche quando non è strettamente necessario.

Si noti che in JUnit 4, l'inizializzazione dell'oggetto di prova avviene subito prima dell'esecuzione del test, quindi l'uso degli inizializzatori di campo è più sicuro e lo stile consigliato.


Interessante. Quindi il comportamento che hai descritto all'inizio si applica solo a JUnit 3?
Craig P. Motlin

6

Nel tuo caso (creazione di un elenco) non c'è differenza nella pratica. Ma generalmente è meglio usare setUp (), perché questo aiuterà Junit a segnalare correttamente le eccezioni. Se si verifica un'eccezione nel costruttore / inizializzatore di un test, questo è un test errore del . Tuttavia, se si verifica un'eccezione durante l'installazione, è naturale considerarla un problema nell'impostazione del test e junit lo segnala in modo appropriato.


1
ben detto. Abituati a istanziare sempre in setUp () e hai una domanda in meno di cui preoccuparti, ad esempio dove dovrei istanziare il mio fooBar, dove la mia collezione. È una sorta di standard di codifica a cui devi solo aderire. Ti avvantaggia non con gli elenchi, ma con altre istanze.
Olaf Kock

@Olaf Grazie per le informazioni sullo standard di codifica, non ci avevo pensato. Tuttavia, tendo a essere più d'accordo con l'idea di Moss Collum di uno standard di codifica.
Craig P. Motlin,

5

Preferisco prima la leggibilità che molto spesso non utilizza il metodo di configurazione. Faccio un'eccezione quando un'operazione di configurazione di base richiede molto tempo e viene ripetuta all'interno di ogni test.
A quel punto sposto quella funzionalità in un metodo di configurazione usando l' @BeforeClassannotazione (ottimizza in seguito).

Esempio di ottimizzazione utilizzando il @BeforeClassmetodo di installazione: utilizzo dbunit per alcuni test funzionali del database. Il metodo di installazione è responsabile di mettere il database in uno stato noto (molto lento ... 30 secondi - 2 minuti a seconda della quantità di dati). Carico questi dati nel metodo di configurazione annotato con @BeforeClasse quindi eseguo 10-20 test sullo stesso set di dati invece di ricaricare / inizializzare il database all'interno di ciascun test.

Usare Junit 3.8 (estendendo TestCase come mostrato nel tuo esempio) richiede la scrittura di un po 'più di codice rispetto alla semplice aggiunta di un'annotazione, ma è ancora possibile "eseguire una volta prima dell'impostazione della classe".


1
+1 perché preferisco anche la leggibilità. Tuttavia, non sono affatto convinto che il secondo modo sia un'ottimizzazione.
Craig P. Motlin

@Motlin ho aggiunto l'esempio dbunit per chiarire come puoi ottimizzare con il setup.
Alex B

Il database è essenzialmente uno stato globale. Quindi spostare l'impostazione del db su setUp () non è un'ottimizzazione, è necessario che i test vengano completati correttamente.
Craig P. Motlin

@ Alex B: Come ha detto Motlin, questa non è un'ottimizzazione. Stai solo cambiando dove nel codice viene eseguita l'inizializzazione, ma non quante volte né quanto velocemente.
Eddie

Intendevo implicare l'uso dell'annotazione "@BeforeClass". Modifica l'esempio per chiarire.
Alex B

2

Poiché ogni test viene eseguito indipendentemente, con una nuova istanza dell'oggetto, non ha molto senso che l'oggetto Test abbia uno stato interno tranne quello condiviso tra setUp()e un singolo test e tearDown(). Questa è una delle ragioni (oltre alle ragioni fornite da altri) per cui è utile utilizzare l'estensionesetUp() metodo.

Nota: è una cattiva idea per un oggetto di prova JUnit mantenere lo stato statico! Se usi la variabile statica nei tuoi test per qualcosa di diverso dal monitoraggio o per scopi diagnostici, stai invalidando parte dello scopo di JUnit, che è che i test possono (e possono) essere eseguiti in qualsiasi ordine, ogni test viene eseguito con un stato fresco e pulito.

I vantaggi dell'utilizzo setUp()sono che non è necessario tagliare e incollare il codice di inizializzazione in ogni metodo di test e che non si dispone del codice di configurazione del test nel costruttore. Nel tuo caso, c'è poca differenza. La semplice creazione di un elenco vuoto può essere eseguita in modo sicuro mentre lo si mostra o nel costruttore poiché è un'inizializzazione banale. Tuttavia, come tu e altri avete sottolineato, tutto ciò che può eventualmente generare un Exceptiondovrebbe essere fatto in setUp()modo da ottenere il dump dello stack diagnostico se fallisce.

Nel tuo caso, dove stai solo creando un elenco vuoto, farei lo stesso modo che suggerisci: Assegna il nuovo elenco al punto della dichiarazione. Soprattutto perché in questo modo hai la possibilità di contrassegnarlo finalse ha senso per la tua classe di prova.


1
+1 perché sei la prima persona a supportare effettivamente l'inizializzazione dell'elenco durante la costruzione dell'oggetto per contrassegnarlo come finale. Tuttavia, le cose sulle variabili statiche sono fuori tema.
Craig P. Motlin,

@Motlin: vero, le cose sulle variabili statiche sono un po 'fuori tema. Non sono sicuro del motivo per cui l'ho aggiunto, ma all'epoca mi sembrava appropriata, un'estensione di ciò che stavo dicendo nel primo paragrafo.
Eddie,

Il vantaggio di finalè menzionato nella domanda però.
Nils von Barth

0
  • I valori costanti (usi in fixture o asserzioni) dovrebbero essere inizializzati nelle loro dichiarazioni e final(poiché non cambiano mai)

  • l'oggetto in prova dovrebbe essere inizializzato nel metodo setup perché possiamo impostare le cose. Ovviamente potremmo non impostare qualcosa ora, ma potremmo impostarlo in seguito. Istanziare nel metodo init faciliterebbe le modifiche.

  • le dipendenze dell'oggetto in prova se queste vengono derise, non dovrebbero nemmeno essere istanziate da te stesso: oggi i mock framework possono istanziarlo per riflessione.

Un test senza dipendenza da deridere potrebbe essere simile a:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Un test con dipendenze da isolare potrebbe essere simile a:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
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.