Pourquoi le fixtureSetup de jUnit doit-il être statique?

109

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 justification? Cela force tous mes init à être sur des champs statiques, sans raison valable pour autant que je vois.

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'elle soit une méthode statique - on ne peut faire exécuter une méthode non statique qu'une seule fois (comme dans NUnit).

ripper234
la source

Réponses:

122

JUnit crée toujours une instance de la classe de test pour chaque méthode @Test. Il s'agit d'une décision de conception fondamentale pour faciliter l'écriture de tests sans effets secondaires. Les bons tests n'ont pas de dépendances d'ordre d'exécution (voir FIRST ) et la création de nouvelles instances de la classe de test et de ses variables d'instance pour chaque test est cruciale pour y parvenir. Certains frameworks de test réutilisent la même instance de classe de test pour tous les tests, ce qui augmente les possibilités de créer accidentellement des effets secondaires entre les tests.

Et comme chaque méthode de test a sa propre instance, cela n'a aucun sens que les méthodes @ BeforeClass / @ AfterClass soient des méthodes d'instance. Sinon, sur laquelle des instances de 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 - les autres auraient les variables d'instance à leurs valeurs par défaut - et le @ La méthode de test serait sélectionnée au hasard, car l'ordre des méthodes dans le fichier .class n'est pas spécifié / dépend du compilateur (IIRC, l'API de réflexion de Java renvoie les méthodes dans le même ordre qu'elles sont déclarées dans le fichier .class, bien que ce comportement soit également n'est pas spécifié - j'ai écrit une bibliothèque pour les trier réellement par leurs numéros de ligne).

Donc, faire en sorte que ces méthodes soient 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 voir, chacun des tests est exécuté avec sa propre instance. Ce que fait JUnit 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();
Esko Luontola
la source
1
"Sinon, sur laquelle des instances de classe de test les méthodes doivent-elles être appelées?" - Sur l'instance de test créée par le test JUnit en cours d'exécution pour exécuter les tests.
HDave
1
Dans cet exemple, il a créé trois instances de test. Il n'y a pas l' instance de test.
Esko Luontola
Oui, je l'ai manqué dans votre exemple. Je pensais plus au moment où JUnit est appelé à partir d'un test exécutant ala Eclipse, ou Spring Test, ou Maven. Dans ces cas, une instance d'une classe de test est créée.
HDave
Non, JUnit crée toujours de nombreuses instances de la classe de test, indépendamment de ce que nous avons utilisé pour lancer les tests. Ce n'est que si vous avez un Runner personnalisé pour une classe de test que quelque chose de différent peut se produire.
Esko Luontola
Bien que je comprenne la décision de conception, je pense qu'elle ne prend pas en compte les besoins commerciaux des utilisateurs. Donc à la fin la décision de conception interne (dont je ne devrais pas me soucier autant en tant qu'utilisateur dès que la bibliothèque fonctionne bien) me force à des choix de conception dans mes tests qui sont vraiment de mauvaises pratiques. Ce n'est vraiment pas du tout agile: D
gicappa
43

La réponse courte est la suivante: il n'y a aucune bonne raison pour que ce 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épendances, l'accès au contexte d'application, la gestion des ressources, la journalisation et tout ce qui dépend de "getClass".

HDave
la source
4
J'ai écrit ma propre superclasse de cas de test et j'utilise les annotations Spring @PostConstructpour la configuration et @AfterClasspour le démontage et j'ignore complètement les annotations statiques de Junit. Pour les tests DAO, j'ai ensuite écrit ma propre TestCaseDataLoaderclasse que j'appelle à partir de ces méthodes.
HDave
9
C'est une réponse terrible, il y a clairement en fait une raison pour qu'elle soit statique, comme l'indique clairement la réponse acceptée. Vous pourriez être en désaccord avec la décision de conception, mais c'est loin de signifier qu'il n'y a "aucune bonne raison" pour la décision.
Adam Parkin
8
Bien sûr, les auteurs de JUnit avaient une raison, je dis que ce n'est pas une bonne raison ... ainsi la source de l'OP (et 44 autres personnes) étant mystifiée. Il aurait été trivial d'utiliser des méthodes d'instance et que les testeurs utilisent une convention pour les appeler. En fin de compte, c'est ce que tout le monde fait pour contourner cette limitation - soit lancez votre propre coureur, soit lancez votre propre classe de test.
HDave
1
@HDave, je pense que votre solution avec @PostConstructet @AfterClassse comporte de la même manière que @Beforeet @After. En fait, vos méthodes seront appelées pour chaque méthode de test et pas une seule fois pour toute la classe (comme Esko Luontola le déclare dans sa réponse, une instance de classe est créée pour chaque méthode de test). Je ne vois pas l'utilité de votre solution donc (sauf si je rate quelque chose)
magnum87
1
Cela fonctionne correctement depuis 5 ans maintenant, donc je pense que ma solution fonctionne.
HDave
14

La documentation de JUnit semble rare, mais je devine: peut-être que JUnit crée une nouvelle instance de votre classe de test avant d'exécuter chaque cas de test, donc la seule façon pour que votre état "fixture" persiste à travers les exécutions est de le rendre statique, ce qui peut être appliquée en vous assurant que votre fixtureSetup (méthode @BeforeClass) est statique.

Blair Conrad
la source
2
Non seulement peut-être, mais JUnit crée définitivement une nouvelle instance d'un cas de test. C'est donc la seule raison.
guerda le
C'est la seule raison pour laquelle ils ont, mais en fait, le coureur Junit pourrait faire le travail d'exécuter les méthodes BeforeTests et AfterTests comme le fait testng.
HDave
TestNG crée-t-il une instance de la classe de test et la partage-t-elle avec tous les tests de la classe? Cela le rend plus vulnérable aux effets secondaires entre les tests.
Esko Luontola du
3

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

Pour y parvenir, vous pouvez utiliser ce modèle:

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

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

Avant (Classe), JPAConnection crée la connexion une fois après (Classe), il la ferme.

getEntityMangerrenvoie une classe interne de JPAConnectionqui implémente EntityManager de jpa et peut accéder à la connexion à l'intérieur du jpaConnection. Avant (test), il commence une transaction après (test), il l'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();
    }
  }
}
MP Korstanje
la source
2

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 @BeforeClass

utilisateur aléatoire
la source
Cela ne semble pas juste comme ça, c'est comme ça. La question est posée depuis de nombreuses années, voici la réponse: martinfowler.com/bliki/JunitNewInstance.html
Paul
1

il existe deux types d'annotations:

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

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

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

production:

------------- Sortie standard ---------------
avant les cours
avant
test 1
après
avant
essai 2
après
après les cours
------------- ---------------- ---------------
dfa
la source
19
Je trouve votre réponse sans importance. Je connais la sémantique de BeforeClass et Before. Cela n'explique pas pourquoi il doit être statique ...
ripper234
1
"Cela force tous mes init à être sur des membres statiques, sans raison valable pour autant que je vois." Ma réponse devrait vous montrer que votre init peut également être non statique en utilisant @Before, au lieu de @BeforeClass
dfa
2
Je voudrais faire une partie de l'initialisation une seule fois, au début de la classe, mais sur des variables non statiques.
ripper234
vous ne pouvez pas avec JUnit, désolé. Vous devez utiliser une variable statique, pas du tout.
dfa le
1
Si l'initialisation est coûteuse, vous pouvez simplement conserver une variable d'état pour enregistrer si vous avez fait l'initialisation, et (vérifier et éventuellement) effectuer l'initialisation dans une méthode @Before ...
Blair Conrad
0

Selon JUnit 5, il semble que la philosophie de création stricte d'une nouvelle instance par méthode de test ait été quelque peu assouplie. 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 de @ 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);
    }
}

imprimerait:

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

Ainsi, vous pouvez réellement instancier des objets une fois par classe de test. Bien sûr, cela fait de votre propre responsabilité d'éviter la mutation des objets instanciés de cette façon.

EJJ
la source
-11

Pour résoudre ce problème, changez simplement la méthode

public void setUpBeforeClass 

à

public static void setUpBeforeClass()

et tout ce qui est défini dans cette méthode à static.

sri
la source
2
Cela ne répond pas du tout à la question.
rgargente le