Dlaczego fixtureSetup jUnita musi być statyczne?

109

Oznaczyłem metodę adnotacją @BeforeClass jUnit i otrzymałem ten wyjątek, który mówi, że musi być statyczny. Jakie jest uzasadnienie? To zmusza wszystkie moje init do działania na statycznych polach, bez żadnego powodu, o ile widzę.

W .Net (NUnit) tak nie jest.

Edycja - fakt, że metoda z adnotacją @BeforeClass działa tylko raz, nie ma nic wspólnego z tym, że jest metodą statyczną - można mieć metodę niestatyczną uruchomioną tylko raz (jak w NUnit).

zrywak234
źródło

Odpowiedzi:

122

JUnit zawsze tworzy jedną instancję klasy testowej dla każdej metody @Test. Jest to podstawowa decyzja projektowa, która ma ułatwić pisanie testów bez skutków ubocznych. Dobre testy nie mają żadnych zależności w kolejności wykonywania (patrz FIRST ), a tworzenie nowych instancji klasy testowej i jej zmiennych instancji dla każdego testu jest kluczowe w osiągnięciu tego. Niektóre platformy testowe wykorzystują tę samą instancję klasy testowej do wszystkich testów, co prowadzi do większej liczby przypadków przypadkowego tworzenia efektów ubocznych między testami.

A ponieważ każda metoda testowa ma swoją własną instancję, nie ma sensu, aby metody @ BeforeClass / @ AfterClass były metodami instancji. W przeciwnym razie, na której instancji klasy testowej należy wywołać metody? Gdyby metody @ BeforeClass / @ AfterClass mogły odwoływać się do zmiennych instancji, to tylko jedna z metod @Test miałaby dostęp do tych samych zmiennych instancji w - reszta miałaby zmienne instancji z wartościami domyślnymi - a @ Metoda testowa byłaby wybierana losowo, ponieważ kolejność metod w pliku .class jest nieokreślona / zależna od kompilatora (IIRC, API odbicia języka Java zwraca metody w tej samej kolejności, w jakiej są zadeklarowane w pliku .class, chociaż również to zachowanie jest nieokreślona - napisałem bibliotekę za faktyczne sortowanie ich według numerów linii).

Zatem wymuszenie statyczności tych metod jest jedynym rozsądnym rozwiązaniem.

Oto przykład:

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

Które wydruki:

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

Jak widać, każdy z testów jest wykonywany z własną instancją. To, co robi JUnit, jest w zasadzie takie samo:

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
źródło
1
„W przeciwnym razie, na której z instancji klasy testowej należy wywołać metody?” - Na instancji testowej utworzonej przez uruchomiony test JUnit w celu wykonania testów.
HDave
1
W tym przykładzie utworzył trzy instancje testowe. Nie ma instancji testowej.
Esko Luontola
Tak - brakowało mi tego w twoim przykładzie. Zastanawiałem się więcej o tym, kiedy JUnit jest wywoływany z testu działającego np. Eclipse, Spring Test lub Maven. W takich przypadkach jest tworzona jedna instancja klasy testowej.
HDave
Nie, JUnit zawsze tworzy wiele instancji klasy testowej, niezależnie od tego, czego użyliśmy do uruchomienia testów. Tylko jeśli masz niestandardowego Runnera dla klasy testowej, może się zdarzyć coś innego.
Esko Luontola
Chociaż rozumiem decyzję projektową, myślę, że nie bierze ona pod uwagę potrzeb biznesowych użytkowników. Na koniec więc wewnętrzna decyzja projektowa (której nie obchodzi mnie tak bardzo jako użytkownik, gdy tylko biblioteka działa dobrze) zmusza mnie do wyborów projektowych w moich testach, które są naprawdę złymi praktykami. To naprawdę nie jest zwinne: D
gicappa,
43

Krótka odpowiedź brzmi: nie ma powodu, aby był statyczny.

W rzeczywistości, uczynienie go statycznym powoduje różnego rodzaju problemy, jeśli używasz Junit do wykonywania testów integracji DAO opartych na DBUnit. Wymaganie statyczne koliduje z wstrzykiwaniem zależności, dostępem do kontekstu aplikacji, obsługą zasobów, rejestrowaniem i wszystkim, co zależy od „getClass”.

HDave
źródło
4
Napisałem własną superklasę przypadków testowych i używam adnotacji Spring @PostConstructdo konfigurowania i @AfterClassniszczenia, a całkowicie ignoruję te statyczne z Junit. Dla testów DAO napisałem następnie własną TestCaseDataLoaderklasę, którą wywołuję z tych metod.
HDave
9
To okropna odpowiedź, najwyraźniej istnieje powód, dla którego ma być statyczna, jak wyraźnie wskazuje przyjęta odpowiedź. Możesz nie zgodzić się z decyzją projektową, ale nie oznacza to, że nie ma „dobrego powodu” dla takiej decyzji.
Adam Parkin
8
Oczywiście autorzy JUnita mieli powód, mówię, że to nie jest dobry powód ... stąd źródło PO (i 44 innych osób) jest zdziwione. Użycie metod instancji byłoby trywialne, a osoby uruchamiające testy stosowałyby konwencję ich wywoływania. W końcu to właśnie robią wszyscy, aby obejść to ograniczenie - albo przeprowadź własny biegacz, albo przeprowadź własną klasę testową.
HDave
1
@HDave, myślę, że twoje rozwiązanie z @PostConstructi @AfterClasszachowuje się tak samo jak @Beforei @After. W rzeczywistości Twoje metody będą wywoływane dla każdej metody testowej, a nie raz dla całej klasy (jak stwierdza Esko Luontola w swojej odpowiedzi, dla każdej metody testowej tworzona jest instancja klasy). Nie widzę użyteczności twojego rozwiązania, więc (chyba że coś mi brakuje)
magnum87
1
Działa poprawnie od 5 lat, więc myślę, że moje rozwiązanie działa.
HDave
13

Dokumentacja JUnit wydaje się skromna, ale zgaduję: być może JUnit tworzy nową instancję Twojej klasy testowej przed uruchomieniem każdego przypadku testowego, więc jedynym sposobem na to, aby stan "fixture" był trwały między przebiegami, jest ustawienie statyczne, co może być egzekwowane, upewniając się, że fixtureSetup (metoda @BeforeClass) jest statyczna.

Blair Conrad
źródło
2
Nie tylko być może, ale JUnit zdecydowanie tworzy nową instancję przypadku testowego. Więc to jest jedyny powód.
guerda
Jest to jedyny powód, dla którego mają, ale w rzeczywistości runner Junit może wykonać zadanie wykonywania metod BeforeTests i AfterTests w sposób, w jaki robi to testng.
HDave
Czy TestNG tworzy jedną instancję klasy testowej i udostępnia ją wszystkim testom w klasie? To sprawia, że ​​jest bardziej podatny na skutki uboczne między testami.
Esko Luontola
3

Chociaż to nie odpowie na pierwotne pytanie. Odpowie na oczywiste dalsze działania. Jak stworzyć regułę działającą przed i po zajęciach oraz przed i po teście.

Aby to osiągnąć, możesz użyć tego wzoru:

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

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

Przed (Class) JPAConnection tworzy połączenie po jego zamknięciu (Class).

getEntityMangerzwraca wewnętrzną klasę, JPAConnectionktóra implementuje EntityManager jpa i może uzyskać dostęp do połączenia wewnątrz jpaConnection. Przed (test) rozpoczyna transakcję, a po (test) ponownie ją wycofuje.

Nie jest to bezpieczne dla wątków, ale można to zrobić.

Wybrany kod 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
źródło
2

Wygląda na to, że JUnit tworzy nową instancję klasy testowej dla każdej metody testowej. Wypróbuj ten kod

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

Wynik to 0 0 0

Oznacza to, że jeśli metoda @BeforeClass nie jest statyczna, to będzie musiała zostać wykonana przed każdą metodą testową i nie będzie możliwości rozróżnienia semantyki @Before i @BeforeClass

randomuser
źródło
Nie tylko tak się wydaje , ale tak jest . Pytanie zadawano od wielu lat, oto odpowiedź: martinfowler.com/bliki/JunitNewInstance.html
Paul
1

istnieją dwa rodzaje adnotacji:

  • @BeforeClass (@AfterClass) wywoływana raz na klasę testową
  • @Before (i @After) wywołane przed każdym testem

więc @BeforeClass musi być zadeklarowane jako statyczne, ponieważ jest wywoływane raz. Należy również wziąć pod uwagę, że bycie statycznym jest jedynym sposobem na zapewnienie właściwej propagacji „stanu” między testami (model JUnit nakłada jedną instancję testową na @Test), a ponieważ w Javie tylko metody statyczne mogą uzyskać dostęp do danych statycznych ... @BeforeClass i @ AfterClass można zastosować tylko do metod statycznych.

Ten przykładowy test powinien wyjaśnić @BeforeClass vs @Before use:

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

wynik:

------------- Wyjście standardowe ---------------
przed zajęciami
przed
test 1
po
przed
test 2
po
po zajęciach
------------- ---------------- ---------------
dfa
źródło
19
Twoja odpowiedź jest nieistotna. Znam semantykę BeforeClass i Before. To nie wyjaśnia, dlaczego musi być statyczny ...
ripper234
1
„To zmusza mój init do tego, aby znajdował się na statycznych elementach, bez żadnego powodu, o ile widzę”. Moja odpowiedź powinna pokazać, że startowych mogą być również non-static użyciu @Before zamiast @BeforeClass
DFA
2
Chciałbym wykonać część init tylko raz, na początku klasy, ale na zmiennych niestatycznych.
ripper234
nie możesz z JUnit, przepraszam. Musisz użyć zmiennej statycznej, nie ma mowy.
dfa
1
Jeśli inicjalizacja jest kosztowna, możesz po prostu zachować zmienną stanu, aby zapisać, czy wykonałeś init, i (sprawdź to i opcjonalnie) wykonać init metodą @Before ...
Blair Conrad
0

Zgodnie z JUnit 5, wydaje się, że filozofia polegająca na ścisłym tworzeniu nowej instancji dla każdej metody testowej została nieco poluzowana. Dodali adnotację, która utworzy instancję klasy testowej tylko raz. W związku z tym ta adnotacja umożliwia również niestatyczne metody z adnotacją @ BeforeAll / @ AfterAll (zamienniki @ BeforeClass / @ AfterClass). Tak więc klasa testowa, taka jak ta:

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

wydrukowałby:

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

Tak więc możesz faktycznie tworzyć instancje obiektów raz na klasę testową. Oczywiście oznacza to, że unikanie mutowania obiektów, które są tworzone w ten sposób, jest Twoją odpowiedzialnością.

EJJ
źródło
-11

Aby rozwiązać ten problem, po prostu zmień metodę

public void setUpBeforeClass 

do

public static void setUpBeforeClass()

i wszystko, co jest zdefiniowane w tej metodzie static.

sri
źródło
2
To wcale nie odpowiada na pytanie.
rgargente,