Why must jUnit's fixtureSetup be static?
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();
The short answer is this: there is no good reason for it to be static.
In fact, making it static causes all sorts of problems if you use Junit to execute DBUnit based DAO integration tests. The static requirement interferes with dependency injection, application context access, resource handling, logging, and anything that depends on "getClass".
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.
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
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();
}
}
}