web-dev-qa-db-fra.com

Pourquoi est-ce que fixtureSetup de jUnit doit être statique?

J'ai marqué une méthode avec l'annotation @BeforeClass de jUnit et j'ai obtenu cette exception disant qu'elle doit être statique. Quelle est la raison? Cela oblige tous mes inits à être sur des champs statiques, sans raison valable, à ma connaissance.

En .Net (NUnit), ce n'est pas le cas.

Edit - le fait qu'une méthode annotée avec @BeforeClass ne s'exécute qu'une seule fois n'a rien à voir avec le fait qu'il s'agit d'une méthode statique - une méthode non statique ne peut être exécutée qu'une seule fois (comme dans NUnit).

102
ripper234

JUnit always crée une instance de la classe de test pour chaque méthode @Test. Il s'agit d'une décision de conception fondamentale } pour faciliter la rédaction de tests sans effets secondaires. Les bons tests n'ont pas de dépendances sur l'ordre d'exécution (voir F.I.R.S.T ) et il est essentiel de créer de nouvelles instances de la classe de test et ses variables d'instance pour chaque test. Certains frameworks de test réutilisent la même instance de classe de test pour tous les tests, ce qui offre davantage de possibilités de créer accidentellement des effets secondaires entre les tests.

Et comme chaque méthode de test possède sa propre instance, il n’a aucun sens de considérer les méthodes @ BeforeClass/@ AfterClass comme des méthodes d’instance. Sinon, à quelle instance de la classe de test les méthodes doivent-elles être appelées? S'il était possible pour les méthodes @ BeforeClass/@ AfterClass de référencer des variables d'instance, alors une seule des méthodes @Test aurait accès à ces mêmes variables d'instance - le reste aurait les variables d'instance à leurs valeurs par défaut. - et la méthode @Test serait sélectionnée de manière aléatoire, car l'ordre des méthodes dans le fichier .class n'est pas spécifié/dépendant du compilateur (IIRC, l'API de réflexion de Java renvoie les méthodes dans le même ordre que celles déclarées dans le fichier .class, bien que ce comportement ne soit pas non plus spécifié - j’ai écrit une bibliothèque pour les trier réellement par leur numéro de ligne).

Donc, imposer à ces méthodes d'être statiques est la seule solution raisonnable.

Voici un exemple:

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");
    }
}

Quelles impressions:

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

Comme vous pouvez le constater, chacun des tests est exécuté avec sa propre instance. Ce que JUnit fait est fondamentalement le même que celui-ci:

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();
116
Esko Luontola

La réponse courte est la suivante: il n’ya aucune bonne raison pour qu’il soit statique.

En fait, le rendre statique pose toutes sortes de problèmes si vous utilisez Junit pour exécuter des tests d'intégration DAO basés sur DBUnit. L'exigence statique interfère avec l'injection de dépendance, l'accès au contexte de l'application, la gestion des ressources, la journalisation et tout ce qui dépend de "getClass".

38
HDave

La documentation de JUnit semble rare, mais je suppose que JUnit crée peut-être une nouvelle instance de votre classe de test avant d'exécuter chaque scénario de test. être appliquée en vous assurant que votre fixtureSetup (méthode @BeforeClass) est statique.

12
Blair Conrad

Bien que cela ne répondra pas à la question initiale. Cela répondra au suivi évident. Comment créer une règle qui fonctionne avant et après une classe et avant et après un test.

Pour ce faire, vous pouvez utiliser ce modèle:

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

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

On before (Class), la connexion JPAC crée la connexion une fois après (Class), elle la ferme.

getEntityManger renvoie une classe interne de JPAConnection qui implémente EntityManager de jpa et peut accéder à la connexion à l'intérieur de jpaConnection. Avant (test), il commence une transaction après (test), il annule à nouveau.

Ce n'est pas thread-safe mais peut être fait pour l'être.

Code sélectionné de 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();
    }
  }
}
3
mpkorstanje

Il semble que JUnit crée une nouvelle instance de la classe de test pour chaque méthode de test. Essayez ce code 

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++);
    }
}

La sortie est 0 0 0

Cela signifie que si la méthode @BeforeClass n'est pas statique, elle devra être exécutée avant chaque méthode de test et il n'y aurait aucun moyen de différencier la sémantique de @Before et de @BeforeClass. 

2
randomuser

il existe deux types d'annotations:

  • @BeforeClass (@AfterClass) appelé une fois par classe de test
  • @Before (et @After) appelé avant chaque test

so @BeforeClass doit être déclaré statique car il est appelé une fois. Vous devez également considérer qu'être statique est le seul moyen de garantir une propagation "d'état" correcte entre les tests (le modèle JUnit impose une instance de test par @Test) et, dans Java, seules les méthodes statiques peuvent accéder aux données statiques ... @BeforeClass et @ AfterClass peut être appliqué uniquement aux méthodes statiques. 

Cet exemple de test devrait clarifier l'utilisation de @BeforeClass vs @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");
    }
}

sortie:

------------- Sortie standard ---------------
 Avant le cours 
 Avant le 
 Test 1 
 après 
 avant 
 test 2 
 après 
 après la classe 
------------- --------------- - ---------------
1
dfa

Selon JUnit 5, il semble que la philosophie de création stricte d’une nouvelle méthode par test ait été quelque peu relâchée. Ils ont ajouté une annotation qui instanciera une classe de test une seule fois. Cette annotation permet donc également aux méthodes annotées avec @ BeforeAll/@ AfterAll (les remplacements à @ BeforeClass/@ AfterClass) d'être non statiques. Donc, une classe de test comme celle-ci:

@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);
    }
}

serait imprimer:

Java.lang.Object@799d4f69
Java.lang.Object@799d4f69

Ainsi, vous pouvez réellement instancier des objets une fois par classe de test. Bien entendu, cela implique que vous ayez la responsabilité d'éviter la mutation d'objets instanciés de cette façon.

0
EJJ