Почему jUnit fixtureSetup должен быть статическим?


109

Я пометил метод аннотацией jUnit @BeforeClass и получил это исключение, в котором говорится, что он должен быть статическим. В чем обоснование? Это заставляет весь мой init работать в статических полях без всякой на то веской причины, насколько я понимаю.

В .Net (NUnit) это не так.

Изменить - тот факт, что метод, помеченный @BeforeClass, запускается только один раз, не имеет ничего общего с тем, что он является статическим методом - нестатический метод может запускаться только один раз (как в NUnit).

Ответы:


122

JUnit всегда создает один экземпляр тестового класса для каждого метода @Test. Это фундаментальное дизайнерское решение, упрощающее написание тестов без побочных эффектов. Хорошие тесты не имеют зависимости от порядка выполнения (см. FIRST ), и создание новых экземпляров тестового класса и его переменных экземпляра для каждого теста имеет решающее значение для достижения этой цели. Некоторые среды тестирования повторно используют один и тот же экземпляр класса тестирования для всех тестов, что увеличивает вероятность случайного создания побочных эффектов между тестами.

И поскольку каждый тестовый метод имеет свой собственный экземпляр, нет смысла использовать методы @ BeforeClass / @ AfterClass в качестве методов экземпляра. В противном случае, для какого из экземпляров тестового класса следует вызывать методы? Если бы методы @ BeforeClass / @ AfterClass могли ссылаться на переменные экземпляра, тогда только один из методов @Test имел бы доступ к тем же самым переменным экземпляра - остальные имели бы переменные экземпляра в их значениях по умолчанию - и @ Метод тестирования будет выбран случайным образом, поскольку порядок методов в файле .class не указан / зависит от компилятора (IIRC, API отражения Java возвращает методы в том же порядке, в каком они объявлены в файле .class, хотя также это поведение не указано - я написал библиотеку для фактической сортировки их по номерам строк).

Так что принудительное использование статических методов - единственное разумное решение.

Вот пример:

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

Какие отпечатки:

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

Как видите, каждый из тестов выполняется с собственным экземпляром. Что делает JUnit, в основном то же самое:

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
«В противном случае, для какого из экземпляров тестового класса следует вызывать методы?» - В тестовом экземпляре, созданном запущенным тестом JUnit для выполнения тестов.
HDave

1
В этом примере он создал три тестовых экземпляра. Существует нет экземпляра теста.
Эско Луонтола

Да, я пропустил это в вашем примере. Я больше думал о том, когда JUnit вызывается из теста, выполняющего аля Eclipse, или Spring Test, или Maven. В этих случаях создается один экземпляр тестового класса.
HDave 01

Нет, JUnit всегда создает множество экземпляров тестового класса, независимо от того, что мы использовали для запуска тестов. Что-то другое может произойти только в том случае, если у вас есть собственный Runner для тестового класса.
Эско Луонтола

Хотя я понимаю дизайнерское решение, я думаю, что оно не принимает во внимание бизнес-потребности пользователей. Итак, в конце концов, внутреннее дизайнерское решение (которое мне, как пользователю, не должно быть настолько важно, если библиотека работает хорошо) заставляет меня выбирать дизайн в моих тестах, что является действительно плохой практикой. Это действительно совсем не гибко: D
gicappa

43

Короткий ответ таков: нет веских причин для статичности.

Фактически, его статичность вызывает всевозможные проблемы, если вы используете Junit для выполнения интеграционных тестов DAO на основе DBUnit. Статическое требование мешает внедрению зависимостей, доступу к контексту приложения, обработке ресурсов, ведению журнала и всему, что зависит от "getClass".


4
Я написал свой собственный суперкласс тестовых примеров и использую аннотации Spring @PostConstructдля настройки и @AfterClassудаления, а статические из Junit я вообще игнорирую. Затем для тестов DAO я написал свой собственный TestCaseDataLoaderкласс, который вызываю из этих методов.
HDave 01

9
Это ужасный ответ, очевидно, что на самом деле есть причина, по которой он статичен, о чем ясно свидетельствует принятый ответ. Вы можете не согласиться с дизайнерским решением, но это далеко не означает, что для такого решения нет «веской причины».
Адам Паркин

8
Конечно, у авторов JUnit была причина, я говорю, что это не веская причина ... таким образом, источник OP (и еще 44 человека) был озадачен. Было бы тривиально использовать методы экземпляра и заставить исполнителей тестов использовать соглашение для их вызова. В конце концов, это то, что делают все, чтобы обойти это ограничение - либо создайте собственный бегун, либо свой собственный тестовый класс.
HDave 01

1
@HDave, я думаю, что ваше решение с @PostConstructи @AfterClassпросто ведет себя так же, как @Beforeи @After. Фактически, ваши методы будут вызываться для каждого тестового метода, а не один раз для всего класса (как утверждает Эско Луонтола в своем ответе, экземпляр класса создается для каждого тестового метода). Я не вижу полезности вашего решения, поэтому (если я что-то не пропущу)
magnum87

1
Он работает правильно уже 5 лет, поэтому я думаю, что мое решение работает.
HDave

14

Документация JUnit кажется скудной, но я предполагаю: возможно, JUnit создает новый экземпляр вашего тестового класса перед запуском каждого тестового примера, поэтому единственный способ сохранить состояние вашего «приспособления» во время выполнения - сделать его статическим, что может принудительно, убедившись, что ваш fixtureSetup (метод @BeforeClass) является статическим.


2
Не только возможно, но JUnit определенно создает новый экземпляр тестового примера. Так что это единственная причина.
guerda

Это единственная причина, по которой у них есть, но на самом деле бегун Junit может выполнять работу по выполнению методов BeforeTests и AfterTests так же, как это делает testng.
HDave

Создает ли TestNG один экземпляр тестового класса и делится им со всеми тестами в классе? Это делает его более уязвимым для побочных эффектов между тестами.
Эско Луонтола

3

Хотя это не ответит на исходный вопрос. Это ответит на очевидное продолжение. Как создать правило, которое работает до и после занятия, до и после теста.

Для этого вы можете использовать этот шаблон:

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

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

До (Класс) JPAConnection создает соединение один раз, после того, как (Класс) закрывает его.

getEntityMangerвозвращает внутренний класс, JPAConnectionкоторый реализует EntityManager jpa и может получить доступ к соединению внутри jpaConnection. До (тест) он начинает транзакцию, после (тест) откатывает ее снова.

Это не является потокобезопасным, но может быть сделано так.

Выбранный код 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

Кажется, что JUnit создает новый экземпляр тестового класса для каждого тестового метода. Попробуйте этот код

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

На выходе 0 0 0

Это означает, что если метод @BeforeClass не является статическим, его нужно будет выполнять перед каждым тестовым методом, и не будет возможности различать семантику @Before и @BeforeClass.


Это не просто кажется , таким образом, это является таким образом. Этот вопрос задают много лет, вот ответ: martinfowler.com/bliki/JunitNewInstance.html
Paul

1

есть два типа аннотаций:

  • @BeforeClass (@AfterClass) один раз для каждого тестового класса
  • @Before (и @After) вызывается перед каждым тестом

поэтому @BeforeClass должен быть объявлен статическим, потому что он вызывается один раз. Вы также должны учитывать, что статичность - это единственный способ гарантировать правильное распространение «состояния» между тестами (модель JUnit требует одного тестового экземпляра для каждого @Test) и, поскольку в Java только статические методы могут получить доступ к статическим данным ... @BeforeClass и @ AfterClass можно применять только к статическим методам.

Этот пример теста должен прояснить использование @BeforeClass и @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");
    }
}

вывод:

------------- Стандартный вывод ---------------
перед классом
перед
тест 1
после
перед
тест 2
после
после школы
------------- ---------------- ---------------

19
Я считаю ваш ответ неуместным. Я знаю семантику BeforeClass и Before. Это не объясняет, почему он должен быть статичным ...
ripper234

1
«Это заставляет всю мою инициализацию быть на статических членах, без какой-либо веской причины, насколько я понимаю». Мой ответ должен показать вам , что ваш инициализационный может быть также нестатическая с использованием @Before, вместо @BeforeClass
DFA

2
Я хотел бы выполнить часть инициализации только один раз, в начале класса, но для нестатических переменных.
ripper234

вы не можете использовать JUnit, извините. Ни в коем случае нельзя использовать статическую переменную.
dfa

1
Если инициализация дорогостоящая, вы можете просто сохранить переменную состояния, чтобы записывать, выполнили ли вы инициализацию, и (проверьте это и, возможно,) выполните инициализацию с помощью метода @Before ...
Блэр Конрад,

0

Согласно JUnit 5, кажется, что философия строгого создания нового экземпляра для каждого метода тестирования была несколько ослаблена. Они добавили аннотацию , которая создает экземпляр тестового класса только один раз. Таким образом, эта аннотация также позволяет методам, аннотированным @ BeforeAll / @ AfterAll (замена @ BeforeClass / @ AfterClass), быть нестатическими. Итак, тестовый класс вроде этого:

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

напечатает:

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

Таким образом, вы можете создавать экземпляры объектов один раз для каждого тестового класса. Конечно, вы сами обязаны избегать мутации объектов, которые создаются таким образом.


-11

Чтобы решить эту проблему, просто измените метод

public void setUpBeforeClass 

к

public static void setUpBeforeClass()

и все, что определено в этом методе static.


2
Это вообще не отвечает на вопрос.
rgargente
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.