Warum muss das fixtureSetup von jUnit statisch sein?


109

Ich habe eine Methode mit der @ BeforeClass-Annotation von jUnit markiert und diese Ausnahme erhalten, die besagt, dass sie statisch sein muss. Was ist die Begründung? Dies zwingt alle meine Init auf statische Felder, soweit ich sehe, ohne guten Grund.

In .Net (NUnit) ist dies nicht der Fall.

Bearbeiten - Die Tatsache, dass eine mit @BeforeClass annotierte Methode nur einmal ausgeführt wird, hat nichts damit zu tun, dass es sich um eine statische Methode handelt. Eine nicht statische Methode kann nur einmal ausgeführt werden (wie in NUnit).

Antworten:


122

JUnit erstellt für jede @ Test-Methode immer eine Instanz der Testklasse. Dies ist eine grundlegende Entwurfsentscheidung , um das Schreiben von Tests ohne Nebenwirkungen zu vereinfachen. Gute Tests haben keine Abhängigkeiten in der Reihenfolge der Ausführung (siehe ERSTE ), und das Erstellen neuer Instanzen der Testklasse und ihrer Instanzvariablen für jeden Test ist entscheidend, um dies zu erreichen. Einige Test-Frameworks verwenden für alle Tests dieselbe Testklasseninstanz wieder, was zu mehr Möglichkeiten führt, versehentlich Nebenwirkungen zwischen Tests zu erzeugen.

Und da jede Testmethode eine eigene Instanz hat, ist es nicht sinnvoll, dass die @ BeforeClass / @ AfterClass-Methoden Instanzmethoden sind. Ansonsten, auf welcher der Testklasseninstanzen sollten die Methoden aufgerufen werden? Wenn die Methoden @ BeforeClass / @ AfterClass auf Instanzvariablen verweisen könnten, hätte nur eine der @ Test-Methoden Zugriff auf dieselben Instanzvariablen - der Rest hätte die Instanzvariablen auf ihren Standardwerten - und die @ Die Testmethode wird zufällig ausgewählt, da die Reihenfolge der Methoden in der .class-Datei nicht angegeben / vom Compiler abhängig ist (IIRC, Javas Reflection-API gibt die Methoden in derselben Reihenfolge zurück, in der sie in der .class-Datei deklariert sind, obwohl auch dieses Verhalten ist nicht spezifiziert - ich habe eine Bibliothek geschrieben um sie tatsächlich nach ihren Zeilennummern zu sortieren).

Daher ist es die einzig vernünftige Lösung, diese Methoden als statisch zu erzwingen.

Hier ist ein Beispiel:

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

Welche Drucke:

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

Wie Sie sehen können, wird jeder der Tests mit einer eigenen Instanz ausgeführt. Was JUnit macht, ist im Grunde dasselbe:

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
"Ansonsten, auf welcher der Testklasseninstanzen sollten die Methoden aufgerufen werden?" - Auf der Testinstanz, die der JUnit-Test erstellt hat, um die Tests auszuführen.
HDave

1
In diesem Beispiel wurden drei Testinstanzen erstellt. Es gibt keine der Testinstanz.
Esko Luontola

Ja - das habe ich in Ihrem Beispiel verpasst. Ich habe mehr darüber nachgedacht, wann JUnit von einem Test aufgerufen wird, der ala Eclipse, Spring Test oder Maven ausführt. In diesen Fällen wird eine Instanz einer Testklasse erstellt.
HDave

Nein, JUnit erstellt immer viele Instanzen der Testklasse, unabhängig davon, was wir zum Starten der Tests verwendet haben. Nur wenn Sie einen benutzerdefinierten Runner für eine Testklasse haben, kann etwas anderes passieren.
Esko Luontola

Obwohl ich die Entwurfsentscheidung verstehe, denke ich, dass sie die Geschäftsanforderungen der Benutzer nicht berücksichtigt. Am Ende zwingt mich die interne Entwurfsentscheidung (die mich als Benutzer nicht so sehr interessieren sollte, sobald die Bibliothek gut funktioniert) zu Entwurfsentscheidungen in meinen Tests, die wirklich schlechte Praktiken sind. Das ist wirklich überhaupt nicht agil: D
Gicappa

43

Die kurze Antwort lautet: Es gibt keinen guten Grund dafür, dass es statisch ist.

Wenn Sie Junit zum Ausführen von DBUnit-basierten DAO-Integrationstests verwenden, verursacht das statische Erstellen allerlei Probleme. Die statische Anforderung beeinträchtigt die Abhängigkeitsinjektion, den Zugriff auf den Anwendungskontext, die Ressourcenbehandlung, die Protokollierung und alles, was von "getClass" abhängt.


4
Ich habe meine eigene Testfall-Superklasse geschrieben und verwende die Spring-Annotationen @PostConstructzum Einrichten und @AfterClassAbreißen. Ich ignoriere die statischen von Junit insgesamt. Für DAO-Tests habe ich dann meine eigene TestCaseDataLoaderKlasse geschrieben, die ich mit diesen Methoden aufrufe.
HDave

9
Das ist eine schreckliche Antwort. Es gibt eindeutig einen Grund dafür, dass sie statisch ist, wie die akzeptierte Antwort eindeutig anzeigt. Sie sind vielleicht nicht mit der Entwurfsentscheidung einverstanden, aber das bedeutet keineswegs, dass es "keinen guten Grund" für die Entscheidung gibt.
Adam Parkin

8
Natürlich hatten die JUnit-Autoren einen Grund, ich sage, es ist kein guter Grund ... daher ist die Quelle der OP (und 44 anderer Leute) mystifiziert. Es wäre trivial gewesen, Instanzmethoden zu verwenden und die Testläufer eine Konvention anwenden zu lassen, um sie aufzurufen. Letztendlich ist es das, was jeder tut, um diese Einschränkung zu umgehen - entweder rollen Sie Ihren eigenen Läufer oder rollen Sie Ihre eigene Testklasse.
HDave

1
@ HDave, ich denke, dass Ihre Lösung mit @PostConstructund sich @AfterClassgenauso verhält wie @Beforeund @After. Tatsächlich werden Ihre Methoden für jede Testmethode und nicht einmal für die gesamte Klasse aufgerufen (wie Esko Luontola in seiner Antwort feststellt, wird für jede Testmethode eine Klasseninstanz erstellt). Ich kann den Nutzen Ihrer Lösung nicht sehen (es sei denn, ich vermisse etwas)
magnum87

1
Es läuft seit 5 Jahren korrekt, daher denke ich, dass meine Lösung funktioniert.
HDave

13

Die JUnit-Dokumentation scheint knapp zu sein, aber ich denke mal: Vielleicht erstellt JUnit eine neue Instanz Ihrer Testklasse, bevor jeder Testfall ausgeführt wird. Die einzige Möglichkeit für Ihren "Fixture" -Status, über mehrere Läufe hinweg zu bestehen, besteht darin, dass er statisch ist, was möglich ist Stellen Sie sicher, dass Ihr fixtureSetup (@ BeforeClass-Methode) statisch ist.


2
Nicht nur vielleicht, sondern JUnit erstellt definitiv eine neue Instanz eines Testfalls. Das ist also der einzige Grund.
Guerda

Dies ist der einzige Grund , warum sie haben, aber in der Tat die Junit Läufer könnte die Arbeit erledigen hat ein BeforeTests und AfterTests Methoden der Weg testng auszuführen.
HDave

Erstellt TestNG eine Instanz der Testklasse und teilt sie mit allen Tests in der Klasse? Das macht es anfälliger für Nebenwirkungen zwischen den Tests.
Esko Luontola

3

Dies wird jedoch die ursprüngliche Frage nicht beantworten. Es wird das offensichtliche Follow-up beantworten. So erstellen Sie eine Regel, die vor und nach einer Klasse sowie vor und nach einem Test funktioniert.

Um dies zu erreichen, können Sie dieses Muster verwenden:

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

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

Vor (Klasse) erstellt die JPAConnection die Verbindung einmal, nachdem (Klasse) sie geschlossen hat.

getEntityMangerJPAConnectionGibt eine innere Klasse zurück , die den EntityManager von jpa implementiert und auf die Verbindung innerhalb von zugreifen kann jpaConnection. Vor (Test) beginnt eine Transaktion, nach (Test) wird sie erneut zurückgesetzt.

Dies ist nicht threadsicher, kann aber so gemacht werden.

Ausgewählter Code von 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

Es scheint, dass JUnit für jede Testmethode eine neue Instanz der Testklasse erstellt. Probieren Sie diesen Code aus

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

Die Ausgabe ist 0 0 0

Dies bedeutet, dass wenn die @ BeforeClass-Methode nicht statisch ist, sie vor jeder Testmethode ausgeführt werden muss und es keine Möglichkeit gibt, zwischen der Semantik von @ Beefore und @ BeeClass zu unterscheiden


Es ist nicht nur scheint so, es ist auf diese Weise. Die Frage wurde seit vielen Jahren gestellt, hier ist die Antwort: martinfowler.com/bliki/JunitNewInstance.html
Paul

1

Es gibt zwei Arten von Anmerkungen:

  • @BeforeClass (@AfterClass) wird einmal pro Testklasse aufgerufen
  • @Before (und @After) werden vor jedem Test aufgerufen

Daher muss @BeforeClass als statisch deklariert werden, da es einmal aufgerufen wird. Sie sollten auch berücksichtigen, dass statisch ist die einzige Möglichkeit, um eine ordnungsgemäße "Status" -Verbreitung zwischen Tests sicherzustellen (das JUnit-Modell schreibt eine Testinstanz pro @Test vor), und da in Java nur statische Methoden auf statische Daten zugreifen können ... @BeforeClass und @ AfterClass kann nur auf statische Methoden angewendet werden.

Dieser Beispieltest sollte die Verwendung von @BeforeClass und @Before klarstellen:

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

Ausgabe:

------------- Standardausgabe ---------------
vor dem Unterricht
Vor
Test 1
nach dem
Vor
Test 2
nach dem
nach dem Unterricht
------------- ---------------- ---------------

19
Ich finde deine Antwort irrelevant. Ich kenne die Semantik von BeforeClass und Before. Dies erklärt nicht, warum es statisch sein muss ...
ripper234

1
"Dies zwingt alle meine Init auf statische Mitglieder, ohne guten Grund, soweit ich sehe." Meine Antwort sollte Ihnen zeigen, dass Ihr Init auch mit @Before nicht statisch sein kann, anstatt mit @BeforeClass
dfa

2
Ich möchte einen Teil des Init nur einmal zu Beginn der Klasse ausführen, jedoch für nicht statische Variablen.
Ripper234

Sie können nicht mit JUnit, sorry. Sie müssen auf keinen Fall eine statische Variable verwenden.
dfa

1
Wenn die Initialisierung teuer ist, können Sie einfach eine Statusvariable behalten, um aufzuzeichnen, ob Sie die Init durchgeführt haben, und (überprüfen Sie sie und optional) die Init in einer @ Before-Methode ausführen ...
Blair Conrad

0

Gemäß JUnit 5 scheint die Philosophie, eine neue Instanz pro Testmethode strikt zu erstellen, etwas gelockert worden zu sein. Sie haben hinzugefügt eine Anmerkung , die nur einmal eine Testklasse instanziiert wird. Diese Annotation ermöglicht daher auch, dass Methoden, die mit @ BeforeAll / @ AfterAll (die Ersetzungen von @ BeforeClass / @ AfterClass) annotiert sind, nicht statisch sind. Also eine Testklasse wie diese:

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

würde drucken:

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

Sie können Objekte also tatsächlich einmal pro Testklasse instanziieren. Dies macht es natürlich zu Ihrer eigenen Verantwortung, zu vermeiden, dass Objekte mutiert werden, die auf diese Weise instanziiert werden.


-11

Um dieses Problem zu beheben, ändern Sie einfach die Methode

public void setUpBeforeClass 

zu

public static void setUpBeforeClass()

und alles, was in dieser Methode definiert ist, um static.


2
Dies beantwortet die Frage überhaupt nicht.
Rgargente
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.