为什么 jUnit 的 fixture 安装必须是静态的?

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

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

编辑 ——用@BeforeClass 注释的方法只运行一次与它是静态方法没有任何关系——一个非静态方法只能运行一次(就像在 NUnit 中一样)。

54957 次浏览

JUnit 文档似乎很少,但是我猜想: 也许 JUnit 在运行每个测试用例之前创建了一个测试类的新实例,所以“ fixture”状态在运行期间持久化的唯一方法就是使它是静态的,这可以通过确保 fixtureSetup (@BeforeClass 方法)是静态的来实现。

there are two types of annotations:

  • @ Before Class (@AfterClass) 每个测试类调用一次
  • @Before (and @After) 在每次测试之前调用

所以@BeforeClass 必须声明为静态的,因为它只被调用一次。您还应该考虑到,静态是确保测试之间正确“状态”传播的唯一方法(JUnit 模型为每个@Test 强制一个测试实例) ,而且,因为在 Java 中只有静态方法可以访问静态数据... ...@BeforeClass 和@AfterClass 只能应用于静态方法。

这个示例测试应该澄清@Before Class 和@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");
}
}

产出:

------------- Standard Output ---------------
before class
before
test 1
after
before
test 2
after
after class
------------- ---------------- ---------------

简短的回答是: 它没有理由是静止的。

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

看起来 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 的语义

JUnit 一直都是为每个@Test 方法创建一个测试类实例。这是一个基本的设计决策使编写没有副作用的测试更加容易。好的测试没有任何运行顺序依赖关系(参见 F.I.R.S.T) ,为每个测试创建测试类及其实例变量的新实例对于实现这一点至关重要。一些测试框架为所有测试重用相同的测试类实例,这导致在测试之间意外地创建副作用的可能性更大。

而且因为每个测试方法都有自己的实例,所以将@BeforeClass/@AfterClass 方法作为实例方法是没有意义的。否则,应该在哪个测试类实例上调用这些方法?如果@BeforeClass/@AfterClass 方法可以引用实例变量,那么 那么只有一个@Test 方法可以访问这些相同的实例变量——其他方法的实例变量都是默认值——@Test 方法将被随机选择,因为。类文件是非指定/编译器依赖的(IIRC,Java 的反射 API 返回的方法与在。类文件,虽然这种行为也没有指定-我已经编写了 图书馆,实际上是按行号对它们进行排序)。

因此,使这些方法保持静态是唯一合理的解决方案。

Here is an example:

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

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

public void setUpBeforeClass

public static void setUpBeforeClass()

以及该方法中定义的所有内容到 static

Though this won't answer the original question. It will answers the obvious follow up. How to create a rule that works before and after a class and before and after a test.

为了达到这个目的,你可以使用这个模式:

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


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

On before(Class) the JPAConnection creates the connection once on after(Class) it closes it.

getEntityManger返回 JPAConnection的一个内部类,该类实现 jpa 的 EntityManager,并且可以访问 jpaConnection内部的连接。在 before (test)时,它开始一个事务; 在 after (test)时,它再次回滚该事务。

这不是线程安全的,但可以这样做。

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

根据 JUnit 5,似乎严格按照每个测试方法创建一个新实例的哲学已经有所松动。他们添加了只实例化一个测试类一次的 注释。因此,这个注释还允许用@beforall/@AfterAll (替身计画@beoreClass/@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

因此,实际上可以为每个测试类实例化一次对象。当然,避免以这种方式实例化的对象发生变化是您自己的责任。