Why must jUnit's fixtureSetup be static?

前端 未结 8 1470
悲&欢浪女
悲&欢浪女 2020-12-04 07:50

I marked a method with jUnit\'s @BeforeClass annotation, and got this exception saying it must be static. What\'s the rationale? This forces all my init to be on static fiel

相关标签:
8条回答
  • 2020-12-04 08:32

    JUnit always creates one instance of the test class for each @Test method. This is a fundamental design decision to make it easier to write tests without side-effects. Good tests do not have any order-of-run dependencies (see F.I.R.S.T) and creating fresh instances of the test class and its instance variables for each test is crucial in achieving this. Some testing frameworks reuse the same test class instance for all tests, which leads to more possibilities of accidentally creating side-effects between tests.

    And because each test method has its own instance, it makes no sense for the @BeforeClass/@AfterClass methods to be instance methods. Otherwise, on which of the test class instances should the methods be called? If it would be possible for the @BeforeClass/@AfterClass methods to reference instance variables, then only one of the @Test methods would have access to those same instance variables - the rest would have the instance variables at their default values - and the @Test method would be randomly selected, because the order of methods in the .class file is unspecified/compiler-dependent (IIRC, Java's reflection API returns the methods in the same order as they are declared in the .class file, although also that behaviour is unspecified - I have written a library for actually sorting them by their line numbers).

    So enforcing those methods to be static is the only reasonable solution.

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

    Which prints:

    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
    

    As you can see, each of the tests is executed with its own instance. What JUnit does is basically the same as this:

    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();
    
    0 讨论(0)
  • 2020-12-04 08:38

    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.

    To achieve that you can use this pattern:

    @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 returns an inner class of JPAConnection that implements jpa's EntityManager and can access the connection inside the jpaConnection. On before (test) it begins a transaction on after (test) it rolls it back again.

    This isn't thread-safe but can be made to be so.

    Selected code of 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();
        }
      }
    }
    
    0 讨论(0)
  • 2020-12-04 08:43

    JUnit documentation seems scarce, but I'll guess: perhaps JUnit creates a new instance of your test class before running each test case, so the only way for your "fixture" state to persist across runs is to have it be static, which can be enforced by making sure your fixtureSetup (@BeforeClass method) is static.

    0 讨论(0)
  • 2020-12-04 08:45

    It seems that JUnit creates a new instance of the test class for each test method. Try this code out

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

    The output is 0 0 0

    This means that if the @BeforeClass method is not static then it will have to be executed before each test method and there would be no way to differentiate between the semantics of @Before and @BeforeClass

    0 讨论(0)
  • 2020-12-04 08:45

    there are two types of annotations:

    • @BeforeClass (@AfterClass) called once per test class
    • @Before (and @After) called before each test

    so @BeforeClass must be declared static because it is called once. You should also consider that being static is the only way to ensure proper "state" propagation between tests (JUnit model imposes one test instance per @Test) and, since in Java only static methods can access static data... @BeforeClass and @AfterClass can be applied only to static methods.

    This example test should clarify @BeforeClass vs @Before usage:

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

    output:

    ------------- Standard Output ---------------
    before class
    before
    test 1
    after
    before
    test 2
    after
    after class
    ------------- ---------------- ---------------
    
    0 讨论(0)
  • 2020-12-04 08:49

    As per JUnit 5, it seems the philosophy on strictly creating a new instance per test method has been somewhat loosened. They have added an annotation that will instantiate a test class only once. This annotation therefore also allows methods annotated with @BeforeAll/@AfterAll (the replacements to @BeforeClass/@AfterClass) to be non-static. So, a test class like this:

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

    would print:

    java.lang.Object@799d4f69
    java.lang.Object@799d4f69
    

    So, you can actually instantiate objects once per test class. Of course, this does make it your own responsibility to avoid mutating objects that are instantiated this way.

    0 讨论(0)
提交回复
热议问题