为什么jUnit的fixtureSetup必须是静态的?
我用jUnit的@BeforeClass注解标记了一个方法,并得到这个exception说它必须是静态的。 基本原理是什么? 这迫使我所有的init都在静态的领域,就我所见,没有任何理由。
在.Net(NUnit)中,情况并非如此。
编辑 – 用@BeforeClass注释的方法只运行一次与静态方法无关 – 可以有一个非静态方法只运行一次(如在NUnit中)。
JUnit 总是为每个@Test方法创build一个testing类的实例。 这是一个基本的devise决策 ,使编写testing更容易,没有副作用。 好的testing没有任何运行顺序依赖关系(见FIRST ),为每个testing创buildtesting类及其实例variables的新实例对于实现这一点至关重要。 一些testing框架为所有testing重复使用相同的testing类实例,从而导致在testing之间意外产生副作用的可能性增加。
而且由于每个testing方法都有自己的实例,因此@ BeforeClass / @ AfterClass方法成为实例方法是没有意义的。 否则,应该在哪个testing类实例中调用方法? 如果@ BeforeClass / @ AfterClass方法可以引用实例variables, 那么只有一个@Test方法可以访问那些相同的实例variables – 其余的实例variables将以默认值进行访问 – 而@testing方法将被随机select,因为.class文件中的方法顺序是未指定的/依赖于编译器(IIRC,Java的reflectionAPI以与它们在.class文件中声明的顺序相同的顺序返回方法,尽pipe也是这种行为没有指定 – 我已经写了一个库 ,实际上是通过它们的行号对它们进行sorting)。
所以强制这些方法是静态的是唯一合理的解决scheme。
这里是一个例子:
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
正如你所看到的,每个testing都是用自己的实例执行的。 JUnit的function基本上与此相同:
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来执行基于DBUnit的DAO集成testing,那么将其设为静态会导致各种各样的问题。 静态需求会干扰dependency injection,应用程序上下文访问,资源处理,日志logging以及任何依赖于“getClass”的东西。
JUnit文档似乎很less,但是我猜测:在运行每个testing用例之前,也许JUnit会创build一个新的testing类实例,所以在“运行”状态下运行的唯一方法是让它静态化,确保fixtureSetup(@BeforeClass方法)是静态的。
有两种types的注释:
- @BeforeClass(@AfterClass) 每个testing类调用一次
- @Before(和@After) 在每次testing之前调用
所以@BeforeClass必须声明为static,因为它被调用一次。 你还应该考虑静态是确保testing之间正确的“状态”传播的唯一方法(JUnit模型对每个@Test施加一个testing实例),并且由于在Java中只有静态方法可以访问静态数据… @BeforeClass和@ AfterClass只能应用于静态方法。
这个示例testing应该澄清@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"); } }
输出:
-------------标准输出--------------- 课前 之前 testing1 后 之前 testing2 后 下课以后 ------------- ---------------- ---------------
看来,JUnit为每个testing方法创build一个新的testing类实例。 试试这个代码
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方法不是静态的,那么它必须在每个testing方法之前执行,并且将无法区分@Before和@BeforeClass的语义
虽然这不会回答原来的问题。 这将回答明显的后续行动。 如何创build一个在课前和课后testing的规则。
为了实现这一点,你可以使用这种模式:
@ClassRule public static JPAConnection jpaConnection = JPAConnection.forUITest("my-persistence-unit"); @Rule public JPAConnection.EntityManager entityManager = jpaConnection.getEntityManager();
在JPAConnection(Class)之前创build连接(Class)之后closures它。
getEntityManger
返回一个实现jpa的EntityManager的JPAConnection
内部类,并可以访问jpaConnection
内部的jpaConnection
。 在之前(testing)它开始一个交易之后(testing)它再次回滚。
这不是线程安全的,但可以做到这一点。
selectJPAConnection.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(); } } }
根据JUnit 5,似乎每个testing方法严格创build一个新实例的理念已经有所松动。 他们添加了一个注释 ,它只会实例化一个testing类。 因此,这个注释还允许使用@ BeforeAll / @ AfterAll(@ BeforeClass / @ AfterClass的replace)注释的方法是非静态的。 所以,像这样的testing类:
@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
所以,你可以实际上每个testing类实例化一次对象。 当然,这样做会避免使用这种方式实例化对象。
要解决此问题,只需更改该方法
public void setUpBeforeClass
至
public static void setUpBeforeClass()
和所有在这个方法中定义的static
。