【问题标题】:Why must jUnit's fixtureSetup be static?为什么 jUnit 的 fixtureSetup 必须是静态的?
【发布时间】:2009-06-27 10:29:53
【问题描述】:

我用 jUnit 的 @BeforeClass 注释标记了一个方法,并得到这个异常说它必须是静态的。理由是什么?这迫使我所有的初始化都在静态字段上,据我所知没有充分的理由。

在 .Net (NUnit) 中,情况并非如此。

编辑 - 使用 @BeforeClass 注释的方法只运行一次这一事实与它是静态方法无关 - 一个非静态方法可以只运行一次(如在 NUnit 中) )。

【问题讨论】:

    标签: java junit


    【解决方案1】:

    JUnit 总是 为每个@Test 方法创建一个测试类的实例。 This is a fundamental design decision 使编写测试更容易而没有副作用。好的测试没有任何运行顺序依赖(参见F.I.R.S.T),并且为每个测试创建新的测试类实例及其实例变量对于实现这一点至关重要。一些测试框架为所有测试重用相同的测试类实例,这导致在测试之间意外产生副作用的可能性更大。

    而且因为每个测试方法都有自己的实例,所以 @BeforeClass/@AfterClass 方法是实例方法是没有意义的。否则,应该在哪个测试类实例上调用方法?如果 @BeforeClass/@AfterClass 方法可以引用实例变量,那么只有一个 @Test 方法可以访问这些相同的实例变量 - 其余的实例变量位于它们的默认值 - 并且 @Test 方法将被随机选择,因为 .class 文件中的方法顺序是未指定/编译器相关的(IIRC,Java 的反射 API 以与它们在 . class 文件,尽管该行为也未指定 - 我已经写了 a library 用于实际按行号对它们进行排序)。

    因此,将这些方法强制为静态是唯一合理的解决方案。

    这是一个例子:

    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();
    

    【讨论】:

    • "否则,应该在哪个测试类实例上调用方法?" -- 在运行的 JUnit 测试为执行测试而创建的测试实例上。
    • 在该示例中,它创建了 三个 测试实例。没有 测试实例。
    • 是的 - 我在你的例子中错过了这一点。我更多地考虑何时从运行 ala Eclipse、Spring Test 或 Maven 的测试调用 JUnit。在这些情况下,会创建一个测试类的实例。
    • 不,JUnit 总是创建大量测试类的实例,不管我们用什么来启动测试。只有当您有一个用于测试类的自定义 Runner 时,才会发生不同的事情。
    • 虽然我理解设计决策,但我认为它没有考虑到用户的业务需求。所以最后,内部设计决策(只要库运行良好,我就不应该像用户那样关心它)迫使我在测试中选择那些非常糟糕的做法。这真的一点都不敏捷:D
    【解决方案2】:

    简短的回答是:没有充分的理由让它是静态的。

    事实上,如果您使用 Junit 执行基于 DBUnit 的 DAO 集成测试,将其设为静态会导致各种问题。静态要求会干扰依赖注入、应用程序上下文访问、资源处理、日志记录以及任何依赖于“getClass”的内容。

    【讨论】:

    • 我编写了自己的测试用例超类,并使用 Spring 注释 @PostConstruct 进行设置,@AfterClass 进行拆除,我完全忽略了来自 Junit 的静态注释。对于 DAO 测试,我编写了自己的 TestCaseDataLoader 类,我从这些方法中调用它。
    • 这是一个糟糕的答案,显然,正如公认的答案清楚地表明,它实际上是静态的。您可能不同意设计决策,但这绝不意味着该决策“没有充分的理由”。
    • 当然,JUnit 作者是有原因的,我是说这不是一个的原因......因此,OP(和其他 44 人)的来源被迷惑了.使用实例方法并让测试运行程序使用约定来调用它们本来是微不足道的。最后,这就是每个人为了解决这个限制所做的事情——要么滚动你自己的跑步者,要么滚动你自己的测试类。
    • @HDave,我认为您使用 @PostConstruct@AfterClass 的解决方案与 @Before@After 的行为相同。实际上,将为每个测试方法调用您的方法,而不是为整个类调用一次(正如 Esko Luontola 在他的回答中所说,为每个测试方法创建一个类的实例)。我看不到您的解决方案的实用性,所以(除非我错过了什么)
    • 它已经正常运行了 5 年,所以我认为我的解决方案有效。
    【解决方案3】:

    JUnit 文档似乎很少,但我猜:也许 JUnit 在运行每个测试用例之前会为您的测试类创建一个新实例,因此您的“夹具”状态在运行中保持不变的唯一方法是让它保持静态,可以通过确保您的 fixtureSetup(@BeforeClass 方法)是静态的来强制执行。

    【讨论】:

    • 不仅可能,而且 JUnit 肯定会创建一个测试用例的新实例。所以这是唯一的原因。
    • 这是他们拥有的唯一原因,但事实上,Junit 运行器可以像 testng 那样完成执行 BeforeTests 和 AfterTests 方法的工作。
    • TestNG 是否会创建一个测试类的实例并与该类中的所有测试共享?这使得它更容易受到测试之间的副作用的影响。
    【解决方案4】:

    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 的语义

    【讨论】:

    【解决方案5】:

    虽然这不会回答最初的问题。它将回答明显的后续问题。如何创建适用于上课前后以及考试前后的规则。

    要实现这一点,您可以使用此模式:

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

    在 before(Class) 上,JPAConnection 在 after(Class) 关闭它时创建一次连接。

    getEntityManger返回一个JPAConnection的内部类,它实现了jpa的EntityManager,可以访问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();
        }
      }
    }
    

    【讨论】:

      【解决方案6】:

      有两种类型的注解:

      • @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 后 下课以后 ------------- ---------------- ----------------

      【讨论】:

      • 我觉得你的回答无关紧要。我知道 BeforeClass 和 Before 的语义。这并不能解释为什么它必须是静态的......
      • "这迫使我所有的 init 都在静态成员上,据我所知没有充分的理由。"我的回答应该告诉你,你的 init 也可以是 non-static 使用 @Before,而不是 @BeforeClass
      • 我只想在课程开始时做一些初始化,但在非静态变量上。
      • 您不能使用 JUnit,抱歉。你必须使用静态变量,没办法。
      • 如果初始化很昂贵,你可以只保留一个状态变量来记录你是否已经完成了初始化,并且(检查它并可选地)在 @Before 方法中执行初始化...
      【解决方案7】:

      根据 JUnit 5,似乎严格地为每个测试方法创建一个新实例的理念有所放松。他们添加了an annotation,它将只实例化一个测试类一次。因此,此注释还允许使用 @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
      

      因此,您实际上可以为每个测试类实例化一次对象。当然,这确实让您有责任避免改变以这种方式实例化的对象。

      【讨论】:

      • 很好地指出了这一点。使用Lifecycle.PER_CLASS 对很多测试类来说很有意义,它消除了所有的静态麻烦
      【解决方案8】:

      要解决这个问题,只需改变方法

      public void setUpBeforeClass 
      

      public static void setUpBeforeClass()
      

      并将此方法中定义的所有内容发送到static

      【讨论】:

      • 这根本不能回答问题。
      猜你喜欢
      • 2014-05-27
      • 2012-04-11
      • 2011-06-30
      • 1970-01-01
      • 1970-01-01
      • 2017-08-23
      • 1970-01-01
      • 2019-02-06
      • 2016-12-18
      相关资源
      最近更新 更多