jUnitのFixtureSetupを静的にする必要があるのはなぜですか?


109

メソッドにjUnitの@BeforeClassアノテーションを付けましたが、静的でなければならないというこの例外が発生しました。根拠は何ですか?これにより、私の知る限りでは、すべての初期化が静的フィールドに強制されます。

.Net(NUnit)では、これは当てはまりません。

編集 -@BeforeClassアノテーションが付けられたメソッドが1回だけ実行されるという事実は、静的メソッドであることとは何の関係もありません-非静的メソッドを1回だけ実行できます(NUnitの場合と同様)。

回答:


122

JUnitは常に、@ Testメソッドごとにテストクラスのインスタンスを1つ作成します。これは、副作用のないテストを作成しやすくするための基本的な設計上の決定です。優れたテストには実行順序の依存関係はなく(FIRSTを参照)、テストを実行するためにテストクラスの新しいインスタンスとそのインスタンス変数を作成することは、これを達成するために重要です。一部のテストフレームワークは、すべてのテストで同じテストクラスインスタンスを再利用するため、テスト間で誤って副作用が発生する可能性が高くなります。

また、各テストメソッドには独自のインスタンスがあるため、@ BeforeClass / @ AfterClassメソッドがインスタンスメソッドであっても意味がありません。それ以外の場合、メソッドを呼び出す必要があるテストクラスインスタンスはどれですか。@ BeforeClass / @ AfterClassメソッドがインスタンス変数を参照できる場合、@ Testメソッドの1つだけが同じインスタンス変数にアクセスでき、残りはデフォルト値のインスタンス変数を持ち、@ .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 2013年

1
その例では、3つのテストインスタンスを作成しました。何もありませんテスト・インスタンスが。
Esko Luontola 2013年

はい-あなたの例ではそれを逃しました。JUnitがEclipse、Spring Test、またはMavenとして実行されているテストから呼び出されるタイミングについてもっと考えていました。これらの場合、テストクラスのインスタンスが1つ作成されます。
HDave 2013年

いいえ、JUnitは、テストの起動に使用したものに関係なく、常にテストクラスの多数のインスタンスを作成します。別のことが発生する可能性があるのは、テストクラスのカスタムランナーがある場合のみです。
Esko Luontola 2013年

私は設計上の決定を理解していますが、ユーザーのビジネスニーズを考慮していないと思います。したがって、最終的には、内部の設計決定(libが適切に機能するようになり次第、ユーザーほど気にする必要はありません)によって、テストでの設計の選択が強制され、本当に悪い習慣になります。それは本当にアジャイルではありません:D
gicappa '19 / 06/19

43

短い答えはこれです: それが静的である正当な理由はありません。

実際、JUnitを使用してDBUnitベースのDAO統合テストを実行する場合、静的にすることであらゆる種類の問題が発生します。静的要件は、依存性注入、アプリケーションコンテキストアクセス、リソース処理、ロギング、および「getClass」に依存するあらゆるものを妨害します。


4
私は独自のテストケーススーパークラスを作成し@PostConstruct、セットアップと@AfterClass破棄にSpringアノテーションを使用し、Junitからの静的アノテーションをすべて無視します。DAOテストでは、TestCaseDataLoaderこれらのメソッドから呼び出す独自のクラスを作成しました。
HDave 2013

9
それはひどい答えです。実際、受け入れられた答えが明確に示すように、それが静的である理由は確かにあります。設計の決定に反対するかもしれませんが、それは決定に「正当な理由がない」という意味ではありません。
アダムパーキン

8
もちろん、JUnitの作者には理由があり、私はそれが正当な理由ではないと言います...したがって、OP(および44人の他の人々)の出​​所が謎に包まれています。インスタンスメソッドを使用し、テストランナーがそれらを呼び出す規則を採用するのは簡単なことです。結局、それは、この制限を回避するために誰もが行うことです-自分のランナーをロールするか、自分のテストクラスをロールします。
HDave

1
@HDaveは、私が考えることであなたのソリューション@PostConstruct@AfterClassちょうど同じ振る舞い@Before@After。実際、メソッドはクラス全体ではなく、テストメソッドごとに呼び出されます(Esko Luontolaが彼の回答で述べているように、クラスのインスタンスは各テストメソッドに対して作成されます)。私はあなたの解決策の有用性が見えないので(私が何かを
見落とさ

1
それは今5年間正しく動作しているので、私のソリューションが機能すると考えています。
HDave

13

JUnitのドキュメントは不足しているようですが、おそらくJUnitは各テストケースを実行する前にテストクラスの新しいインスタンスを作成するので、「フィクスチャ」の状態を実行間で持続させる唯一の方法は、それを静的にすることです。あなたのfixtureSetup(@BeforeClassメソッド)が静的であることを確認することによって強制されます。


2
多分だけでなく、JUnitは間違いなくテストケースの新しいインスタンスを作成します。これが唯一の理由です。
guerda

これが唯一の理由ですが、JUnit ランナー、testngと同じように、BeforeTestsメソッドとAfterTestsメソッドを実行できます。
HDave

TestNGはテストクラスのインスタンスを1つ作成し、それをクラス内のすべてのテストと共有しますか?これにより、テスト間の副作用に対して脆弱になります。
Esko Luontola、2010

3

これは元の質問には答えませんが。それは明らかなフォローアップに答えます。クラスの前後とテストの前後で機能するルールを作成する方法。

これを実現するには、次のパターンを使用できます。

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

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

before(Class)では、JPAConnectionはafter(Class)で一度接続を作成して閉じます。

getEntityManger の内部クラスを返します JPAConnectionjpaの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++);
    }
}

出力は0 0 0です

つまり、@ BeforeClassメソッドが静的でない場合、各テストメソッドの前に実行する必要があり、@ Beforeと@BeforeClassのセマンティクスを区別する方法はありません。


それはそのように見えるだけでなく、そうです。質問は長年にわたって尋ねられてきました、ここに答えがあります:martinfowler.com/bliki/JunitNewInstance.html
ポール

1

注釈には2つのタイプがあります。

  • @BeforeClass(@AfterClass)はテストクラスごとに1回呼び出されます
  • 各テストの前に呼び出される @Before(および@After)

したがって、@ BeforeClassは一度呼び出されるため、静的に宣言する必要があります。また、静的であることはテスト間の適切な「状態」伝播を保証する唯一の方法であることを考慮する必要があります(JUnitモデルは@Testごとに1つのテストインスタンスを課します)。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
「これにより、私の知る限りでは、すべての初期化が静的メンバーに強制されます。私の答えは、@ BeforeClassの代わりに@Beforeを使用してinitを非静的にすることもできることを示しているはずです
dfa

2
クラスの開始時に、静的でない変数に対して、いくつかの初期化を一度だけ実行したいと思います。
ripper234

JUnitではできません。静的変数を使用する必要があります。
DFA

1
初期化に負荷がかかる場合は、状態変数を保持してinitを実行したかどうかを記録し、(それをチェックして、必要に応じて)@Beforeメソッドでinitを実行します...
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

したがって、実際には、テストクラスごとに1回オブジェクトをインスタンス化できます。もちろん、この方法でインスタンス化されたオブジェクトの変更を回避することは、ユーザー自身の責任になります。


-11

この問題を解決するには、メソッドを変更するだけです

public void setUpBeforeClass 

public static void setUpBeforeClass()

そして、すべてこの方法で定義されていますstatic


2
これは質問にまったく答えません。
rgargente 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.