jUnit의 fixtureSetup이 정적이어야하는 이유는 무엇입니까?


109

jUnit의 @BeforeClass 주석으로 메서드를 표시했는데 정적이어야한다는 예외가 발생했습니다. 그 이유는 무엇입니까? 이것은 내가 보는 한 정당한 이유없이 모든 init이 정적 필드에 있도록 강제합니다.

.Net (NUnit)에서는 그렇지 않습니다.

편집 -@BeforeClass로 주석이 달린 메소드가 한 번만 실행된다는 사실은 정적 메소드와 관련이 없습니다. 비 정적 메소드를 한 번만 실행할 수 있습니다 (NUnit에서와 같이).

답변:


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 년

예-귀하의 예에서 그것을 놓쳤습니다. Eclipse, Spring Test 또는 Maven을 실행하는 테스트에서 JUnit이 호출되는 경우에 대해 더 많이 생각하고있었습니다. 이러한 경우 생성 된 테스트 클래스의 인스턴스가 하나 있습니다.
HDave 2013

아니요, JUnit은 테스트를 시작하는 데 사용한 방법에 관계없이 항상 테스트 클래스의 많은 인스턴스를 만듭니다. 테스트 클래스에 대한 사용자 지정 Runner가있는 경우에만 다른 일이 발생할 수 있습니다.
Esko Luontola 2013

디자인 결정을 이해하는 동안 사용자의 비즈니스 요구를 고려하지 않은 것 같습니다. 결국 내부 디자인 결정 (lib가 잘 작동하자마자 사용자만큼 신경 쓰지 말아야 함)은 테스트에서 정말 나쁜 관행 인 디자인 선택을 강제합니다. D : 그건 전혀 민첩 정말 아니다
gicappa

43

짧은 대답은 이것입니다. 정적 일 이유는 없습니다.

실제로 Junit을 사용하여 DBUnit 기반 DAO 통합 테스트를 실행하면 정적으로 만들면 모든 종류의 문제가 발생합니다. 정적 요구 사항은 종속성 주입, 애플리케이션 컨텍스트 액세스, 리소스 처리, 로깅 및 "getClass"에 의존하는 모든 것을 방해합니다.


4
나는 내 자신의 테스트 케이스 수퍼 클래스를 작성하고 @PostConstruct설정 및 @AfterClass해체를 위해 Spring 주석 을 사용하고 Junit의 정적 주석 을 모두 무시합니다. DAO 테스트의 경우 TestCaseDataLoader이러한 메서드에서 호출하는 자체 클래스 를 작성했습니다 .
HDave 2013

9
그것은 끔찍한 대답입니다. 실제로 수용된 대답이 명확하게 나타내 듯이 정적 인 이유가 있습니다. 디자인 결정에 동의하지 않을 수 있지만 결정에 "합당한 이유가 없음"을 의미하는 것은 아닙니다.
Adam Parkin 2013 년

8
물론 JUnit 작성자에게는 이유가 있습니다. 좋은 이유 가 아니라고 말하고 있습니다 . 따라서 OP (및 다른 44 명)의 출처가 미스터리 화되었습니다. 인스턴스 메서드를 사용하고 테스트 실행자가 규칙을 사용하여 호출하도록하는 것은 사소한 일이었습니다. 결국이 제한을 해결하기 위해 모든 사람이 수행하는 작업입니다. 자신의 러너를 굴 리거나 자신의 테스트 클래스를 굴립니다.
HDave 2013

1
@HDave, 나는 생각을 가진 솔루션 @PostConstruct@AfterClass마찬가지로 같은 행동하라 @Before@After. 실제로 메서드는 전체 클래스에 대해 한 번이 아니라 각 테스트 메서드에 대해 호출됩니다 (Esko Luontola가 답변에서 언급했듯이 각 테스트 메서드에 대해 클래스 인스턴스가 생성됨). 그래서 솔루션의 유용성 (내가 뭔가를 그리워하지 않는 한) 볼 수 없습니다
magnum87

1
현재 5 년 동안 올바르게 실행되고 있으므로 내 솔루션이 작동한다고 생각합니다.
HDave

13

JUnit 문서는 드물게 보이지만 아마도 JUnit은 각 테스트 케이스를 실행하기 전에 테스트 클래스의 새 인스턴스를 생성 할 것입니다. 따라서 "fixture"상태가 실행 중에 유지되는 유일한 방법은 정적 상태를 유지하는 것입니다. fixtureSetup (@BeforeClass 메서드)이 정적인지 확인하여 적용됩니다.


2
뿐만 아니라 JUnit은 확실히 테스트 케이스의 새로운 인스턴스를 생성합니다. 그래서 이것이 유일한 이유입니다.
guerda

이것이 그들이 가진 유일한 이유이지만 실제로 Junit 러너 testng가 수행하는 방식으로 BeforeTests 및 AfterTests 메서드를 실행하는 작업을 수행 할 수 있습니다.
HDave

TestNG는 테스트 클래스의 하나의 인스턴스를 생성하고 클래스의 모든 테스트와 공유합니까? 따라서 테스트 간의 부작용에 더 취약합니다.
Esko Luontola

3

이것은 원래의 질문에 대한 답은 아니지만. 명백한 후속 조치에 답할 것입니다. 수업 전후 및 테스트 전후에 작동하는 규칙을 만드는 방법.

이를 위해 다음 패턴을 사용할 수 있습니다.

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

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

before (Class)에서 JPAConnection은 연결을 종료 한 후 (Class)에서 한 번 연결을 생성합니다.

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

출력은 000

즉, @BeforeClass 메서드가 정적이 아니라면 각 테스트 메서드 전에 실행해야하며 @Before와 @BeforeClass의 의미를 구별 할 방법이 없습니다.


그냥하지 않는 것 같다 그런 식으로, 그것은 이다 그런 식으로. 이 질문은 수년 동안 요청되었습니다. 여기에 답변이 있습니다. martinfowler.com/bliki/JunitNewInstance.html
Paul

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가 정적 멤버에 있도록 강제합니다." 내 대답은 @BeforeClass 대신 @Before를 사용 하여 init이 정적이 아닐 수도 있음을 보여줄 것입니다
dfa

2
클래스 시작시, 비 정적 변수에서 한 번만 init를 수행하고 싶습니다.
ripper234 2009-06-27

JUnit으로는 할 수 없습니다. 죄송합니다. 절대로 정적 변수를 사용해야합니다.
dfa 2009-06-27

1
초기화 비용이 많이 드는 경우 상태 변수를 유지하여 초기화를 수행했는지 여부를 기록하고 (확인하고 선택적으로) @Before 메서드에서 초기화를 수행 할 수 있습니다.
Blair Conrad

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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.