JUnit'in fixtureSetup'ı neden statik olmalıdır?


109

Bir yöntemi jUnit'in @BeforeClass ek açıklamasıyla işaretledim ve statik olması gerektiğini söyleyen bu istisnayı aldım. Gerekçe nedir? Bu, gördüğüm kadarıyla iyi bir sebep olmaksızın tüm initimi statik alanlarda olmaya zorluyor.

.Net'te (NUnit) durum böyle değildir.

Düzenleme - @BeforeClass ile açıklamalı bir yöntemin yalnızca bir kez çalışmasının, bunun statik bir yöntem olmakla ilgisi olmadığı gerçeği - statik olmayan bir yöntemin yalnızca bir kez çalıştırılması olabilir (NUnit'te olduğu gibi).

Yanıtlar:


122

JUnit , her @Test yöntemi için her zaman test sınıfının bir örneğini oluşturur. Bu, yan etkiler olmadan test yazmayı kolaylaştıran temel bir tasarım kararıdır . İyi testlerin herhangi bir çalışma sırası bağımlılığı yoktur ( İLK'e bakın ) ve test sınıfının yeni örneklerini ve her test için örnek değişkenlerini oluşturmak, bunu başarmak için çok önemlidir. Bazı test çerçeveleri, tüm testler için aynı test sınıfı örneğini yeniden kullanır, bu da testler arasında yanlışlıkla yan etkilerin yaratılmasına yönelik daha fazla olasılığa yol açar.

Ve her test yönteminin kendi örneği olduğundan, @ BeforeClass / @ AfterClass yöntemlerinin örnek yöntemler olması anlamsızdır. Aksi takdirde, yöntemler hangi test sınıfı örneklerinde çağrılmalıdır? @ BeforeClass / @ AfterClass yöntemlerinin örnek değişkenlerine başvurması mümkün olsaydı, @Test yöntemlerinden yalnızca biri aynı örnek değişkenlerine erişebilirdi - geri kalanının varsayılan değerlerinde örnek değişkenleri olurdu - ve @ .Class dosyasındaki yöntemlerin sırası belirtilmemiş / derleyiciye bağlı olduğundan (IIRC, Java'nın yansıma API'si yöntemleri .class dosyasında bildirildikleri sırayla döndürdüğünden, test yöntemi rastgele seçilecektir, ancak bu davranış da aynıdır) belirtilmedi - bir kütüphane yazdım satır numaralarına göre sıralamak için).

Dolayısıyla, bu yöntemleri statik olmaya zorlamak tek makul çözümdür.

İşte bir örnek:

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

Hangi baskılar:

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

Gördüğünüz gibi, testlerin her biri kendi örneğiyle yürütülüyor. JUnit'in yaptığı şey temelde bununla aynıdır:

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
"Aksi takdirde, yöntemler hangi test sınıfı örneklerinde çağrılmalıdır?" - Testleri yürütmek için JUnit testinin çalıştığı test örneğinde.
HDave

1
Bu örnekte, üç test örneği oluşturdu . Hiçbir yoktur testi örneği.
Esko Luontola

Evet - örneğinizde bunu kaçırdım. JUnit'in ala Eclipse veya Spring Test veya Maven çalıştıran bir testten ne zaman çağrıldığını düşünüyordum. Bu durumlarda, oluşturulan bir test sınıfının bir örneği vardır.
HDave

Hayır, JUnit, testleri başlatmak için ne kullandığımıza bakılmaksızın her zaman test sınıfının çok sayıda örneğini oluşturur. Sadece bir test sınıfı için özel bir Runner'ınız varsa farklı bir şey olabilir.
Esko Luontola

Tasarım kararını anlasam da, bunun kullanıcıların iş ihtiyaçlarını hesaba katmadığını düşünüyorum. Sonuçta, iç tasarım kararı (ki bu, lib iyi çalışır çalışmaz bir kullanıcı kadar umursamamalıyım), testlerimde gerçekten kötü uygulamalar olan tasarım seçimleri yapmamı sağlıyor. Bu gerçekten çevik değil: D
gicappa

43

Kısa cevap şudur: statik olması için iyi bir neden yoktur.

Aslında, DBUnit tabanlı DAO entegrasyon testlerini yürütmek için Junit'i kullanırsanız, onu statik hale getirmek her türlü soruna neden olur. Statik gereksinim, bağımlılık ekleme, uygulama bağlam erişimi, kaynak işleme, günlüğe kaydetme ve "getClass" a bağlı her şeyi engeller.


4
Kendi test durumu süper sınıfımı yazdım ve Bahar notlarını @PostConstructkurulum ve @AfterClassyıkım için kullandım ve Junit'teki statik olanları tamamen görmezden geliyorum. DAO testleri için daha sonra TestCaseDataLoaderbu yöntemlerden çağırdığım kendi sınıfımı yazdım .
HDave 01

9
Bu korkunç bir cevap, açıkça kabul edilen cevabın açıkça gösterdiği gibi, bunun statik olmasının bir nedeni var. Tasarım kararına katılmayabilirsiniz, ancak bu, karar için "iyi bir neden" olmadığı anlamına gelmez.
Adam Parkin

8
Elbette JUnit yazarlarının bir nedeni vardı, bunun iyi bir nedeni olmadığını söylüyorum ... bu nedenle OP'nin kaynağı (ve diğer 44 kişi) şaşkınlığa uğruyor. Örnek yöntemlerini kullanmak ve test koşucularının bunları çağırmak için bir kural kullanmasını sağlamak önemsiz olurdu. Sonunda, bu sınırlamayı çözmek için herkesin yaptığı şey bu - ya kendi koşucunuzu yuvarlayın ya da kendi test sınıfınızı tamamlayın.
HDave

1
@HDave, Bence ile çözüm @PostConstructve @AfterClasssadece aynı davranırlar @Beforeve @After. Aslında, yöntemleriniz tüm sınıf için bir kez değil her test yöntemi için çağrılacaktır (Esko Luontola'nın yanıtında belirttiği gibi, her test yöntemi için bir sınıf örneği oluşturulur). Ben bu yüzden çözümün programını (Ben bir şey kaçırmak sürece) göremiyorum
magnum87

1
5 yıldır doğru çalışıyor, bu yüzden çözümümün işe yaradığını düşünüyorum.
HDave

14

JUnit dokümantasyonu az görünüyor, ancak tahmin edeceğim: Belki de JUnit, her bir test senaryosunu çalıştırmadan önce test sınıfınızın yeni bir örneğini oluşturur, bu nedenle "fikstür" durumunuzun çalıştırmalar boyunca devam etmesinin tek yolu, statik olması, fixtureSetup'ınızın (@BeforeClass yöntemi) statik olduğundan emin olarak uygulanmalıdır.


2
Sadece belki değil, JUnit kesinlikle bir test senaryosunun yeni bir örneğini oluşturur. Yani tek sebep bu.
guerda

Sahip olmalarının tek nedeni budur, ancak aslında Junit koşucusu , testin yaptığı gibi bir BeforeTests ve AfterTests yöntemini uygulama işini yapabilirdi.
HDave

TestNG, test sınıfının bir örneğini oluşturup sınıftaki tüm testlerle paylaşıyor mu? Bu, testler arasındaki yan etkilere karşı daha savunmasız hale getirir.
Esko Luontola

3

Yine de bu orijinal soruyu cevaplamayacak. Bariz takibe cevap verecektir. Bir dersten önce ve sonra ve bir testten önce ve sonra çalışan bir kural nasıl oluşturulur?

Bunu başarmak için şu kalıbı kullanabilirsiniz:

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

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

(Sınıf) öncesinde, JPAConnection bağlantıyı (Sınıf) kapattıktan sonra bir kez oluşturur.

getEntityMangerJPAConnectionjpa'nın EntityManager'ını uygulayan bir iç sınıfı döndürür ve jpaConnection. (Test) öncesinde bir işleme başlar (test) sonrasında tekrar geri alır.

Bu iş parçacığı için güvenli değildir, ancak öyle yapılabilir.

Seçilen kod 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

Görünüşe göre JUnit, her test yöntemi için test sınıfının yeni bir örneğini oluşturuyor. Bu kodu deneyin

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

Çıkış 0 0 0

Bu, @BeforeClass yöntemi statik değilse, her test yönteminden önce çalıştırılması gerekeceği ve @Before ile @BeforeClass arasında ayrım yapmanın bir yolu olmayacağı anlamına gelir.


Öyle görünmüyor , bu da öyle. Soru uzun yıllardır soruldu, işte cevap: martinfowler.com/bliki/JunitNewInstance.html
Paul

1

iki tür ek açıklama vardır:

  • @BeforeClass (@AfterClass) test sınıfı başına bir kez çağrıldı
  • @Before (ve @After) her testten önce arandı

yani @BeforeClass bir kez çağrıldığından statik olarak bildirilmelidir. Ayrıca, testler arasında uygun "durum" yayılmasını sağlamanın tek yolunun statik olmanın olduğunu da göz önünde bulundurmalısınız (JUnit modeli @ Test başına bir test örneği uygular) ve Java'da yalnızca statik yöntemler statik verilere erişebilir ... @BeforeClass ve @ AfterClass yalnızca statik yöntemlere uygulanabilir.

Bu örnek test, @BeforeClass vs @Before kullanımı açıklığa kavuşturmalıdır:

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

çıktı:

------------- Standart Çıkış ---------------
dersten önce
önce
test 1
sonra
önce
test 2
sonra
dersten sonra
------------- ---------------- ---------------

19
Cevabınızı alakasız buluyorum. BeforeClass ve Before'ın anlamlarını biliyorum. Bu, neden statik olması gerektiğini
açıklamıyor

1
"Bu, gördüğüm kadarıyla iyi bir sebep olmaksızın tüm initimi statik üyelerde olmaya zorluyor." Cevabım size, init'inizin @BeforeClass yerine @Before kullanılarak da statik olmayabileceğini göstermelidir
dfa

2
Init'in bir kısmını yalnızca bir kez, sınıfın başında, ancak statik olmayan değişkenler üzerinde yapmak istiyorum.
ripper234

JUnit ile yapamazsınız, üzgünüm. Kesinlikle statik bir değişken kullanmalısınız.
dfa

1
Başlatma pahalıysa, başlatma işlemini yapıp yapmadığınızı kaydetmek için bir durum değişkenini tutabilir ve (kontrol edip isteğe bağlı olarak) bir @Before yönteminde başlatma işlemini gerçekleştirebilirsiniz ...
Blair Conrad

0

JUnit 5'e göre, test yöntemi başına kesinlikle yeni bir örnek oluşturma felsefesi biraz gevşetilmiş görünüyor. Bir test sınıfını yalnızca bir kez başlatacak bir açıklama eklediler . Dolayısıyla bu açıklama, @ BeforeAll / @ AfterAll ile açıklama eklenen yöntemlerin (@ BeforeClass / @ AfterClass'a yapılan değişiklikler) statik olmamasına da izin verir. Yani, bunun gibi bir test sınıfı:

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

yazdıracaktı:

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

Böylece, aslında her test sınıfı için bir kez nesneleri başlatabilirsiniz. Elbette bu, bu şekilde somutlaştırılan nesneleri mutasyona uğratmaktan kaçınmayı kendi sorumluluğunuz yapar.


-11

Bu sorunu çözmek için sadece yöntemi değiştirin

public void setUpBeforeClass 

-e

public static void setUpBeforeClass()

ve bu yöntemde tanımlanan her şey static.


2
Bu soruya hiç cevap vermiyor.
rgargente
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.