Perché fixtureSetup di jUnit deve essere statico?


109

Ho contrassegnato un metodo con l'annotazione @BeforeClass di jUnit e ho ricevuto questa eccezione dicendo che deve essere statica. Qual è la logica? Questo costringe tutti i miei init a stare su campi statici, senza una buona ragione per quanto vedo.

In .Net (NUnit), questo non è il caso.

modificare : il fatto che un metodo annotato con @BeforeClass venga eseguito solo una volta non ha nulla a che fare con il fatto che sia un metodo statico: si può avere un metodo non statico eseguito solo una volta (come in NUnit).

Risposte:


122

JUnit crea sempre un'istanza della classe di test per ogni metodo @Test. Questa è una decisione progettuale fondamentale per semplificare la scrittura di test senza effetti collaterali. I buoni test non hanno dipendenze dell'ordine di esecuzione (vedi PRIMO ) e la creazione di nuove istanze della classe di test e delle sue variabili di istanza per ogni test è cruciale per raggiungere questo obiettivo. Alcuni framework di test riutilizzano la stessa istanza della classe di test per tutti i test, il che porta a maggiori possibilità di creare accidentalmente effetti collaterali tra i test.

E poiché ogni metodo di test ha la propria istanza, non ha senso che i metodi @ BeforeClass / @ AfterClass siano metodi di istanza. Altrimenti, su quale delle istanze della classe di test dovrebbero essere chiamati i metodi? Se fosse possibile per i metodi @ BeforeClass / @ AfterClass fare riferimento a variabili di istanza, allora solo uno dei metodi @Test avrebbe accesso a quelle stesse variabili di istanza - il resto avrebbe le variabili di istanza ai loro valori predefiniti - e il @ Il metodo di test verrebbe selezionato in modo casuale, perché l'ordine dei metodi nel file .class non è specificato / dipendente dal compilatore (IIRC, l'API di riflessione di Java restituisce i metodi nello stesso ordine in cui sono dichiarati nel file .class, sebbene anche quel comportamento non è specificato - ho scritto una libreria per ordinarli effettivamente in base ai numeri di riga).

Quindi imporre che questi metodi siano statici è l'unica soluzione ragionevole.

Ecco un esempio:

public class ExampleTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("beforeClass");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("afterClass");
    }

    @Before
    public void before() {
        System.out.println(this + "\tbefore");
    }

    @After
    public void after() {
        System.out.println(this + "\tafter");
    }

    @Test
    public void test1() {
        System.out.println(this + "\ttest1");
    }

    @Test
    public void test2() {
        System.out.println(this + "\ttest2");
    }

    @Test
    public void test3() {
        System.out.println(this + "\ttest3");
    }
}

Che stampa:

beforeClass
ExampleTest@3358fd70    before
ExampleTest@3358fd70    test1
ExampleTest@3358fd70    after
ExampleTest@6293068a    before
ExampleTest@6293068a    test2
ExampleTest@6293068a    after
ExampleTest@22928095    before
ExampleTest@22928095    test3
ExampleTest@22928095    after
afterClass

Come puoi vedere, ciascuno dei test viene eseguito con la propria istanza. Quello che fa JUnit è fondamentalmente lo stesso di questo:

ExampleTest.beforeClass();

ExampleTest t1 = new ExampleTest();
t1.before();
t1.test1();
t1.after();

ExampleTest t2 = new ExampleTest();
t2.before();
t2.test2();
t2.after();

ExampleTest t3 = new ExampleTest();
t3.before();
t3.test3();
t3.after();

ExampleTest.afterClass();

1
"Altrimenti, su quale delle istanze della classe di test dovrebbero essere chiamati i metodi?" - Sull'istanza di test che il test JUnit in esecuzione ha creato per eseguire i test.
HDave

1
In quell'esempio sono state create tre istanze di test. Non esiste l' istanza di test.
Esko Luontola

Sì, mi è mancato questo nel tuo esempio. Stavo pensando di più a quando JUnit viene invocato da un test che esegue ala Eclipse, o Spring Test o Maven. In questi casi è stata creata un'istanza di una classe di test.
HDave il

No, JUnit crea sempre molte istanze della classe di test, indipendentemente da ciò che abbiamo utilizzato per avviare i test. È solo se hai un Runner personalizzato per una classe di prova che potrebbe accadere qualcosa di diverso.
Esko Luontola

Pur comprendendo la decisione di progettazione, penso che non tenga conto delle esigenze aziendali degli utenti. Quindi alla fine la decisione di progettazione interna (che non dovrei preoccuparmi così tanto come utente non appena la libreria funziona bene) mi costringe a scelte di progettazione nei miei test che sono davvero cattive pratiche. Non è affatto agile: D
gicappa

43

La risposta breve è questa: non c'è una buona ragione per essere statica.

In effetti, renderlo statico causa tutti i tipi di problemi se si utilizza Junit per eseguire test di integrazione DAO basati su DBUnit. Il requisito statico interferisce con l'inserimento delle dipendenze, l'accesso al contesto dell'applicazione, la gestione delle risorse, la registrazione e tutto ciò che dipende da "getClass".


4
Ho scritto la mia superclasse di test case e uso le annotazioni Spring @PostConstructper la configurazione e @AfterClassper lo smontaggio e ignoro del tutto quelle statiche di Junit. Per i test DAO ho quindi scritto la mia TestCaseDataLoaderclasse che invoco da questi metodi.
HDave il

9
Questa è una risposta terribile, chiaramente c'è in effetti una ragione per essere statica come indica chiaramente la risposta accettata. Potresti non essere d'accordo con la decisione di progettazione, ma questo è ben lungi dal implicare che non ci sia "alcuna buona ragione" per la decisione.
Adam Parkin

8
Ovviamente gli autori di JUnit avevano una ragione, sto dicendo che non è una buona ragione ... quindi la fonte dell'OP (e di altre 44 persone) è stata mistificata. Sarebbe stato banale usare metodi di istanza e fare in modo che i test runner utilizzassero una convenzione per chiamarli. Alla fine, è quello che fanno tutti per aggirare questa limitazione: lancia il tuo corridore o lancia la tua lezione di prova.
HDave il

1
@HDave, penso che la tua soluzione con @PostConstructe si @AfterClasscomporti allo stesso modo di @Beforee @After. Infatti, i tuoi metodi verranno chiamati per ogni metodo di test e non una volta per l'intera classe (come afferma Esko Luontola nella sua risposta, viene creata un'istanza di classe per ogni metodo di test). Non riesco a vedere l'utilità della tua soluzione quindi (a meno che non mi perda qualcosa)
magnum87

1
Funziona correttamente da 5 anni ormai, quindi penso che la mia soluzione funzioni.
HDave il

13

La documentazione di JUnit sembra scarsa, ma immagino: forse JUnit crea una nuova istanza della tua classe di test prima di eseguire ogni caso di test, quindi l'unico modo per il tuo stato "fixture" di persistere attraverso le esecuzioni è che sia statico, il che può essere applicato assicurandoti che il tuo fixtureSetup (metodo @BeforeClass) sia statico.


2
Non solo forse, ma JUnit crea sicuramente una nuova istanza di un test case. Quindi questa è l'unica ragione.
guerda

Questa è l'unica ragione che hanno, ma in realtà il runner Junit potrebbe svolgere il lavoro di eseguire un metodo BeforeTests e AfterTests come fa testng.
HDave

TestNG crea un'istanza della classe di test e la condivide con tutti i test nella classe? Ciò lo rende più vulnerabile agli effetti collaterali tra i test.
Esko Luontola

3

Anche se questo non risponde alla domanda originale. Risponderà all'ovvio seguito. Come creare una regola che funzioni prima e dopo una lezione e prima e dopo un test.

Per ottenere ciò puoi utilizzare questo modello:

@ClassRule
public static JPAConnection jpaConnection = JPAConnection.forUITest("my-persistence-unit");

@Rule
public JPAConnection.EntityManager entityManager = jpaConnection.getEntityManager();

Prima di (Classe) la JPAConnection crea la connessione una volta dopo (Classe) la chiude.

getEntityMangerrestituisce una classe interna JPAConnectionche implementa EntityManager di jpa e può accedere alla connessione all'interno di jpaConnection. Prima di (test) inizia una transazione e dopo (test) la ripristina di nuovo.

Questo non è thread-safe ma può essere fatto per essere così.

Codice selezionato di JPAConnection.class

package com.triodos.general.junit;

import com.triodos.log.Logger;
import org.jetbrains.annotations.NotNull;
import org.junit.rules.ExternalResource;

import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;
import java.util.HashMap;
import java.util.Map;

import static com.google.common.base.Preconditions.checkState;
import static com.triodos.dbconn.DB2DriverManager.DRIVERNAME_TYPE4;
import static com.triodos.dbconn.UnitTestProperties.getDatabaseConnectionProperties;
import static com.triodos.dbconn.UnitTestProperties.getPassword;
import static com.triodos.dbconn.UnitTestProperties.getUsername;
import static java.lang.String.valueOf;
import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

public final class JPAConnectionExample extends ExternalResource {

  private static final Logger LOG = Logger.getLogger(JPAConnectionExample.class);

  @NotNull
  public static JPAConnectionExample forUITest(String persistenceUnitName) {
    return new JPAConnectionExample(persistenceUnitName)
        .setManualEntityManager();
  }

  private final String persistenceUnitName;
  private EntityManagerFactory entityManagerFactory;
  private javax.persistence.EntityManager jpaEntityManager = null;
  private EntityManager entityManager;

  private JPAConnectionExample(String persistenceUnitName) {
    this.persistenceUnitName = persistenceUnitName;
  }

  @NotNull
  private JPAConnectionExample setEntityManager(EntityManager entityManager) {
    this.entityManager = entityManager;
    return this;
  }

  @NotNull
  private JPAConnectionExample setManualEntityManager() {
    return setEntityManager(new RollBackAfterTestEntityManager());
  }


  @Override
  protected void before() {
    entityManagerFactory = Persistence.createEntityManagerFactory(persistenceUnitName, createEntityManagerProperties());
    jpaEntityManager = entityManagerFactory.createEntityManager();
  }

  @Override
  protected void after() {

    if (jpaEntityManager.getTransaction().isActive()) {
      jpaEntityManager.getTransaction().rollback();
    }

    if(jpaEntityManager.isOpen()) {
      jpaEntityManager.close();
    }
    // Free for garbage collection as an instance
    // of EntityManager may be assigned to a static variable
    jpaEntityManager = null;

    entityManagerFactory.close();
    // Free for garbage collection as an instance
    // of JPAConnection may be assigned to a static variable
    entityManagerFactory = null;
  }

  private Map<String,String> createEntityManagerProperties(){
    Map<String, String> properties = new HashMap<>();
    properties.put("javax.persistence.jdbc.url", getDatabaseConnectionProperties().getURL());
    properties.put("javax.persistence.jtaDataSource", null);
    properties.put("hibernate.connection.isolation", valueOf(TRANSACTION_READ_UNCOMMITTED));
    properties.put("hibernate.connection.username", getUsername());
    properties.put("hibernate.connection.password", getPassword());
    properties.put("hibernate.connection.driver_class", DRIVERNAME_TYPE4);
    properties.put("org.hibernate.readOnly", valueOf(true));

    return properties;
  }

  @NotNull
  public EntityManager getEntityManager(){
    checkState(entityManager != null);
    return entityManager;
  }


  private final class RollBackAfterTestEntityManager extends EntityManager {

    @Override
    protected void before() throws Throwable {
      super.before();
      jpaEntityManager.getTransaction().begin();
    }

    @Override
    protected void after() {
      super.after();

      if (jpaEntityManager.getTransaction().isActive()) {
        jpaEntityManager.getTransaction().rollback();
      }
    }
  }

  public abstract class EntityManager extends ExternalResource implements javax.persistence.EntityManager {

    @Override
    protected void before() throws Throwable {
      checkState(jpaEntityManager != null, "JPAConnection was not initialized. Is it a @ClassRule? Did the test runner invoke the rule?");

      // Safety-close, if failed to close in setup
      if (jpaEntityManager.getTransaction().isActive()) {
        jpaEntityManager.getTransaction().rollback();
        LOG.error("EntityManager encountered an open transaction at the start of a test. Transaction has been closed but should have been closed in the setup method");
      }
    }

    @Override
    protected void after() {
      checkState(jpaEntityManager != null, "JPAConnection was not initialized. Is it a @ClassRule? Did the test runner invoke the rule?");
    }

    @Override
    public final void persist(Object entity) {
      jpaEntityManager.persist(entity);
    }

    @Override
    public final <T> T merge(T entity) {
      return jpaEntityManager.merge(entity);
    }

    @Override
    public final void remove(Object entity) {
      jpaEntityManager.remove(entity);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey) {
      return jpaEntityManager.find(entityClass, primaryKey);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties) {
      return jpaEntityManager.find(entityClass, primaryKey, properties);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode) {
      return jpaEntityManager.find(entityClass, primaryKey, lockMode);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode, Map<String, Object> properties) {
      return jpaEntityManager.find(entityClass, primaryKey, lockMode, properties);
    }

    @Override
    public final <T> T getReference(Class<T> entityClass, Object primaryKey) {
      return jpaEntityManager.getReference(entityClass, primaryKey);
    }

    @Override
    public final void flush() {
      jpaEntityManager.flush();
    }

    @Override
    public final void setFlushMode(FlushModeType flushMode) {
      jpaEntityManager.setFlushMode(flushMode);
    }

    @Override
    public final FlushModeType getFlushMode() {
      return jpaEntityManager.getFlushMode();
    }

    @Override
    public final void lock(Object entity, LockModeType lockMode) {
      jpaEntityManager.lock(entity, lockMode);
    }

    @Override
    public final void lock(Object entity, LockModeType lockMode, Map<String, Object> properties) {
      jpaEntityManager.lock(entity, lockMode, properties);
    }

    @Override
    public final void refresh(Object entity) {
      jpaEntityManager.refresh(entity);
    }

    @Override
    public final void refresh(Object entity, Map<String, Object> properties) {
      jpaEntityManager.refresh(entity, properties);
    }

    @Override
    public final void refresh(Object entity, LockModeType lockMode) {
      jpaEntityManager.refresh(entity, lockMode);
    }

    @Override
    public final void refresh(Object entity, LockModeType lockMode, Map<String, Object> properties) {
      jpaEntityManager.refresh(entity, lockMode, properties);
    }

    @Override
    public final void clear() {
      jpaEntityManager.clear();
    }

    @Override
    public final void detach(Object entity) {
      jpaEntityManager.detach(entity);
    }

    @Override
    public final boolean contains(Object entity) {
      return jpaEntityManager.contains(entity);
    }

    @Override
    public final LockModeType getLockMode(Object entity) {
      return jpaEntityManager.getLockMode(entity);
    }

    @Override
    public final void setProperty(String propertyName, Object value) {
      jpaEntityManager.setProperty(propertyName, value);
    }

    @Override
    public final Map<String, Object> getProperties() {
      return jpaEntityManager.getProperties();
    }

    @Override
    public final Query createQuery(String qlString) {
      return jpaEntityManager.createQuery(qlString);
    }

    @Override
    public final <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery) {
      return jpaEntityManager.createQuery(criteriaQuery);
    }

    @Override
    public final <T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass) {
      return jpaEntityManager.createQuery(qlString, resultClass);
    }

    @Override
    public final Query createNamedQuery(String name) {
      return jpaEntityManager.createNamedQuery(name);
    }

    @Override
    public final <T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass) {
      return jpaEntityManager.createNamedQuery(name, resultClass);
    }

    @Override
    public final Query createNativeQuery(String sqlString) {
      return jpaEntityManager.createNativeQuery(sqlString);
    }

    @Override
    public final Query createNativeQuery(String sqlString, Class resultClass) {
      return jpaEntityManager.createNativeQuery(sqlString, resultClass);
    }

    @Override
    public final Query createNativeQuery(String sqlString, String resultSetMapping) {
      return jpaEntityManager.createNativeQuery(sqlString, resultSetMapping);
    }

    @Override
    public final void joinTransaction() {
      jpaEntityManager.joinTransaction();
    }

    @Override
    public final <T> T unwrap(Class<T> cls) {
      return jpaEntityManager.unwrap(cls);
    }

    @Override
    public final Object getDelegate() {
      return jpaEntityManager.getDelegate();
    }

    @Override
    public final void close() {
      jpaEntityManager.close();
    }

    @Override
    public final boolean isOpen() {
      return jpaEntityManager.isOpen();
    }

    @Override
    public final EntityTransaction getTransaction() {
      return jpaEntityManager.getTransaction();
    }

    @Override
    public final EntityManagerFactory getEntityManagerFactory() {
      return jpaEntityManager.getEntityManagerFactory();
    }

    @Override
    public final CriteriaBuilder getCriteriaBuilder() {
      return jpaEntityManager.getCriteriaBuilder();
    }

    @Override
    public final Metamodel getMetamodel() {
      return jpaEntityManager.getMetamodel();
    }
  }
}

2

Sembra che JUnit crei una nuova istanza della classe di test per ogni metodo di test. Prova questo codice

public class TestJunit
{

    int count = 0;

    @Test
    public void testInc1(){
        System.out.println(count++);
    }

    @Test
    public void testInc2(){
        System.out.println(count++);
    }

    @Test
    public void testInc3(){
        System.out.println(count++);
    }
}

L'uscita è 0 0 0

Ciò significa che se il metodo @BeforeClass non è statico, dovrà essere eseguito prima di ogni metodo di test e non ci sarebbe modo di distinguere tra la semantica di @Before e @BeforeClass


Esso non solo sembra in questo modo, si è in questo modo. La domanda è stata posta per molti anni, ecco la risposta: martinfowler.com/bliki/JunitNewInstance.html
Paul

1

ci sono due tipi di annotazioni:

  • @BeforeClass (@AfterClass) chiamato una volta per classe di test
  • @Before (e @After) hanno chiamato prima di ogni test

quindi @BeforeClass deve essere dichiarato statico perché viene chiamato una volta. Dovresti anche considerare che essere statici è l'unico modo per garantire una corretta propagazione dello "stato" tra i test (il modello JUnit impone un'istanza di test per @Test) e, poiché in Java solo i metodi statici possono accedere ai dati statici ... @BeforeClass e @ AfterClass può essere applicato solo a metodi statici.

Questo test di esempio dovrebbe chiarire l'utilizzo di @BeforeClass rispetto a @Before:

public class OrderTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("before class");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("after class");
    }

    @Before
    public void before() {
        System.out.println("before");
    }

    @After
    public void after() {
        System.out.println("after");
    }    

    @Test
    public void test1() {
        System.out.println("test 1");
    }

    @Test
    public void test2() {
        System.out.println("test 2");
    }
}

produzione:

------------- Standard output ---------------
prima della lezione
prima
prova 1
dopo
prima
prova 2
dopo
dopo la lezione
------------- ---------------- ---------------

19
Trovo la tua risposta irrilevante. Conosco la semantica di BeforeClass e Before. Questo non spiega perché deve essere statico ...
ripper234

1
"Questo costringe tutti i miei init a stare su membri statici, senza una buona ragione per quanto ne so." La mia risposta dovrebbe mostrarti che il tuo init può essere anche non statico usando @Before, invece di @BeforeClass
dfa

2
Vorrei fare un po 'di init solo una volta, all'inizio della classe, ma su variabili non statiche.
ripper234

non puoi con JUnit, mi dispiace. Devi usare una variabile statica, in nessun modo.
DFA

1
Se l'inizializzazione è costosa, potresti semplicemente tenere una variabile di stato per registrare se hai eseguito l'init e (controllalo e opzionalmente) eseguire l'init in un metodo @Before ...
Blair Conrad

0

Secondo JUnit 5, sembra che la filosofia sulla creazione rigorosa di una nuova istanza per metodo di test sia stata leggermente allentata. Hanno aggiunto un'annotazione che istanzerà una classe di test solo una volta. Questa annotazione pertanto consente anche ai metodi annotati con @ BeforeAll / @ AfterAll (le sostituzioni di @ BeforeClass / @ AfterClass) di essere non statici. Quindi, una classe di test come questa:

@TestInstance(Lifecycle.PER_CLASS)
class TestClass() {
    Object object;

    @BeforeAll
    void beforeAll() {
        object = new Object();
    }

    @Test
    void testOne() {
        System.out.println(object);
    }

    @Test
    void testTwo() {
        System.out.println(object);
    }
}

stamperà:

java.lang.Object@799d4f69
java.lang.Object@799d4f69

Quindi, puoi effettivamente istanziare gli oggetti una volta per classe di test. Naturalmente, questo rende tua responsabilità evitare di mutare oggetti che sono istanziati in questo modo.


-11

Per risolvere questo problema è sufficiente modificare il metodo

public void setUpBeforeClass 

per

public static void setUpBeforeClass()

e tutto ciò che è definito in questo metodo a static.


2
Questo non risponde affatto alla domanda.
rgargente
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.