Чому пристрій jUnit має бути статичним?


109

Я позначив метод за допомогою анотації @BeforeClass jUnit і отримав цей виняток, кажучи, що він повинен бути статичним. Яке обґрунтування? Це змушує весь мій ініт знаходитись на статичних полях, без поважних причин, наскільки я бачу.

У .Net (NUnit) це не так.

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

Відповіді:


122

JUnit завжди створює один екземпляр тестового класу для кожного методу @Test. Це фундаментальне дизайнерське рішення для спрощення написання тестів без побічних ефектів. Хороші тести не мають ніяких залежностей від порядку виконання (див. ПЕРШИЙ ), і створення нових екземплярів тестового класу та його змінних екземплярів для кожного тесту має вирішальне значення для досягнення цього. Деякі рамки тестування використовують повторно один і той же екземпляр тестового класу для всіх тестів, що призводить до більшої можливості випадкового створення побічних ефектів між тестами.

Оскільки у кожного методу тестування є власний екземпляр, немає сенсу для методів @ 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 викликають тест, що працює під час ala Eclipse, або Spring Test, або Maven. У цих випадках створено один екземпляр тестового класу.
HDave

Ні, JUnit завжди створює безліч екземплярів тестового класу, незалежно від того, що ми використовували для запуску тестів. Щось таке, якщо у вас є власний Runner для тестового класу, може статися щось інше.
Esko Luontola

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

43

Коротка відповідь така: немає вагомих причин, щоб він був статичним.

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


4
Я написав власний суперкласс тестового випадку і використовую анотації Spring @PostConstructдля налаштування і @AfterClassдля зриву, і я взагалі ігнорую статичні з Junit. Тоді для тестів DAO я написав власний TestCaseDataLoaderклас, до якого я посилаюся з цих методів.
HDave

9
Це жахлива відповідь, очевидно, що насправді є причина, щоб вона була статичною, як чітко вказує прийнята відповідь. Ви можете не погодитися з дизайнерським рішенням, але це далеко не означає, що для рішення не існує "вагомих причин".
Адам Паркін

8
Звичайно , автори JUnit була причина, я говорю сво НЕ хороша причина ... тому джерело ОП (і 44 інших людей) був спантеличений. Небезпечно було б використовувати методи екземплярів, а тестові бігуни використовуватимуть умову для їх виклику. Зрештою, це роблять усі, щоб подолати це обмеження - або катайте власного бігуна, або виконайте свій власний тестовий клас.
HDave

1
@HDave, я думаю, що ваше рішення з @PostConstructі @AfterClassпросто поводиться так само, як @Beforeі @After. Насправді ваші методи будуть викликатися для кожного методу тестування, а не один раз для всього класу (як стверджує Еско Луонтола у своїй відповіді, для кожного методу тесту створюється екземпляр класу). Я не бачу корисності вашого рішення (якщо я щось не пропускаю)
magnum87,

1
Він працює правильно вже 5 років, тому я думаю, що моє рішення працює.
HDave

13

Документація JUnit здається дефіцитною, але я здогадуюсь: можливо, JUnit створює новий екземпляр свого тестового класу перед запуском кожного тестового випадку, тож єдиний спосіб, щоб ваш стан "кріплення" зберігався протягом прогонів, - це стати статичним, що може Виконуйте примусовий контроль, переконавшись, що ваш кріплення (метод @BeforeClass) є статичним.


2
Не тільки можливо, але JUnit безумовно створює новий екземпляр тестового випадку. Тож це єдина причина.
guerda

Це єдина з них причин, але насправді бігун Junit міг би виконати роботу по виконанню методів BeforeTests та AfterTests, як це робить тест.
HDave

Чи створює TestNG один екземпляр тестового класу і ділиться ним з усіма тестами в класі? Це робить його більш вразливим до побічних ефектів між тестами.
Еско Луонтола

3

Хоча це не відповість на початкове запитання. Це дасть відповідь на очевидні подальші дії. Як створити правило, яке працює до і після заняття та до і після тесту.

Щоб досягти цього, ви можете використовувати цю схему:

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

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

Якщо раніше (Клас), JPAConnection створює з'єднання один раз після (Class), воно закриває його.

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
Павло

1

є два типи приміток:

  • @BeforeClass (@AfterClass) викликається один раз за тестовий клас
  • @Before (і @After) телефонував перед кожним тестом

тому @BeforeClass потрібно оголосити статичним, оскільки він викликається один раз. Також слід врахувати, що статичність - це єдиний спосіб забезпечити належне поширення "стану" між тестами (модель JUnit накладає один тестовий екземпляр на @Test), і оскільки в Java лише статичні методи можуть отримати доступ до статичних даних ... @BeforeClass і @ AfterClass можна застосовувати лише до статичних методів.

Цей приклад тесту повинен уточнити використання @BeforeClass vs @Before:

public class OrderTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("before class");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("after class");
    }

    @Before
    public void before() {
        System.out.println("before");
    }

    @After
    public void after() {
        System.out.println("after");
    }    

    @Test
    public void test1() {
        System.out.println("test 1");
    }

    @Test
    public void test2() {
        System.out.println("test 2");
    }
}

вихід:

------------- Стандартний вихід ---------------
перед уроком
раніше
тест 1
після
раніше
тест 2
після
після занять
------------- ---------------- ---------------

19
Я вважаю вашу відповідь несуттєвою. Я знаю семантику BeforeClass і Before. Це не пояснює, чому це має бути статичним ...
ripper234

1
"Це змушує весь мій ініт бути на статичних членах, без поважних причин, наскільки я бачу". Моя відповідь повинна показати вам, що ваш ініт може бути також нестатичним, використовуючи @Before, замість @BeforeClass
dfa

2
Я хотів би зробити деякі з init лише один раз, на початку заняття, але на нестатичних змінних.
ripper234

Ви не можете з JUnit, вибачте. Ви повинні використовувати статичну змінну, ніяк.
dfa

1
Якщо ініціалізація дорога, ви можете просто зберегти змінну стану, щоб записати, чи зробили ви інітит, і (перевірте це та необов'язково) виконувати init методом @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.