为什么jUnit的fixtureSetup必须是静态的?


109

我用jUnit的@BeforeClass批注标记了一个方法,并得到了这个异常,说它必须是静态的。有什么根据?据我所知,这没有任何充分理由就迫使我的所有init都处于静态字段上。

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

编辑 -使用@BeforeClass注释的方法只能运行一次的事实与它作为静态方法无关-可以使非静态方法仅运行一次(与NUnit一样)。

Answers:


122

JUnit 总是为每个@Test方法创建一个测试类的实例。这是一个基本的设计决策,可以使编写测试变得更容易而没有副作用。好的测试没有任何运行顺序依赖关系(请参阅FIRST),为每个测试创建测试类及其实例变量的新实例对于实现这一点至关重要。一些测试框架将相同的测试类实例重用于所有测试,这导致了在测试之间意外产生副作用的更多可能性。

并且由于每个测试方法都有自己的实例,因此@ BeforeClass / @ AfterClass方法成为实例方法是没有意义的。否则,应在哪个测试类实例上调用这些方法?如果@ BeforeClass / @ AfterClass方法可以引用实例变量,则只有@Test方法之一可以访问这些相同的实例变量 -其余的实例变量将使用其默认值-而@测试方法将是随机选择的,因为.class文件中的方法顺序是未指定/依赖于编译器的(IIRC,Java的反射API以与.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
在该示例中,它创建了三个测试实例。没有任何测试实例。
Esko Luontola 2013年

是的-在您的示例中我错过了这一点。我在考虑何时从运行ala Eclipse或Spring Test或Maven的测试中调用JUnit。在这些情况下,将创建一个测试类的实例。
HDave

不,无论我们用来启动测试什么,JUnit都会创建很多测试类的实例。只有对于测试类具有自定义的Runner时,可能会发生不同的事情。
Esko Luontola

虽然我了解设计决策,但我认为它没有考虑到用户的业务需求。因此,最后,内部设计决策(只要lib运行良好,我就不会太在乎用户了),这迫使我在测试中选择确实是不好的做法的设计选择。:D
gicappa

43

简短的答案是: 没有充分的理由使它保持静态。

实际上,如果您使用Junit执行基于DBUnit的DAO集成测试,则使其静态化会引起各种问题。静态需求会干扰依赖项注入,应用程序上下文访问,资源处理,日志记录以及任何依赖于“ getClass”的事物。


4
我编写了自己的测试用例超类,并使用Spring注释@PostConstruct进行设置和@AfterClass拆卸,而我完全忽略了Junit中的静态注释。对于DAO测试,我然后编写了自己的TestCaseDataLoader类,该类从这些方法中调用。
HDave

9
这是一个很糟糕的答案,很明显,事实上,有一个理由使它成为静态,这是公认的答案清楚表明的。您可能不同意设计决策,但这远非意味着没有“充分理由”进行决策。
亚当·帕金

8
当然,JUnit作者是有原因的,我说这不是一个很好的理由……因此,OP(以及其他44个人)的来源被神秘化了。使用实例方法并让测试运行者采用约定来调用它们是微不足道的。最后,这就是每个人为了解决此限制而要做的事情-要么滚动自己的跑步程序,要么滚动自己的测试课程。
HDave

1
@HDave,我认为,随着您的解决方案@PostConstruct@AfterClass刚刚的行为一样@Before@After。实际上,将为每个测试方法调用您的方法,而不是为整个类调用一次(正如Esko Luontola在其回答中指出的那样,将为每个测试方法创建一个类的实例)。我看不到您的解决方案的实用程序(除非我错过了什么)
magnum87 2015年

1
它已经正确运行了5年,所以我认为我的解决方案有效。
HDave 2015年

13

JUnit文档似乎很稀少,但我会猜测:也许JUnit在运行每个测试用例之前会创建一个新的测试类实例,因此,“ fixture”状态在运行期间持久存在的唯一方法是使其为静态,这可以通过确保您的fixtureSetup(@BeforeClass方法)是静态的来强制执行。


2
JUnit不仅可能,而且肯定会创建一个测试用例的新实例。因此,这是唯一的原因。
guerda

这是他们拥有的唯一原因,但是实际上Junit运行程序可以像testng一样执行执行BeforeTests和AfterTests方法的工作。
HDave 2010年

TestNG是否创建测试类的一个实例并与该类中的所有测试共享?这使得它更容易遭受测试之间的副作用。
Esko Luontola

3

虽然这不能回答原始问题。它将回答明显的后续行动。如何创建在课程前后,测试前后均适用的规则。

为此,您可以使用以下模式:

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

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与@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
“就我所知,这没有充分的理由就迫使我的所有init成为静态成员。” 我的回答应该告诉您,您可以使用@Before而不是@BeforeClass 来使自己的初始化成为非静态
dfa

2
我只想在课程开始时一次执行一些init,但要对非静态变量进行一次。
ripper234 2009年

对不起,您无法使用JUnit。绝对不能使用静态变量。
dfa

1
如果初始化的开销很大,则可以保留一个状态变量以记录是否已完成初始化,并(通过检查)(可选)使用@Before方法执行初始化...
Blair Conrad,2009年

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 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.