Mengapa fixtureSetup jUnit harus statis?

109

Saya menandai metode dengan anotasi @BeforeClass jUnit, dan mendapat pengecualian ini mengatakan itu harus statis. Apa alasannya? Ini memaksa semua init saya berada di medan statis, tanpa alasan yang bagus sejauh yang saya lihat.

Dalam .Net (NUnit), hal ini tidak terjadi.

Edit - fakta bahwa metode yang dianotasi dengan @BeforeClass hanya berjalan sekali tidak ada hubungannya dengan metode statis - seseorang dapat menjalankan metode non-statis hanya sekali (seperti di NUnit).

ripper234
sumber

Jawaban:

122

JUnit selalu membuat satu instance kelas pengujian untuk setiap metode @Test. Ini adalah keputusan desain mendasar untuk mempermudah penulisan tes tanpa efek samping. Pengujian yang baik tidak memiliki dependensi order-of-run (lihat FIRST ) dan membuat instance baru dari kelas pengujian dan variabel instance-nya untuk setiap pengujian sangat penting untuk mencapai hal ini. Beberapa framework pengujian menggunakan kembali instance kelas pengujian yang sama untuk semua pengujian, yang mengarah pada lebih banyak kemungkinan menciptakan efek samping secara tidak sengaja di antara pengujian.

Dan karena setiap metode pengujian memiliki instance sendiri, tidak masuk akal jika metode @ BeforeClass / @ AfterClass menjadi metode instance. Jika tidak, pada instance kelas pengujian manakah metode yang harus dipanggil? Jika memungkinkan untuk metode @ BeforeClass / @ AfterClass untuk mereferensikan variabel instans, maka hanya salah satu metode @Test yang akan memiliki akses ke variabel instans yang sama - sisanya akan memiliki variabel instans pada nilai defaultnya - dan @ Metode pengujian akan dipilih secara acak, karena urutan metode dalam file .class tidak ditentukan / compiler-dependent (IIRC, API refleksi Java mengembalikan metode dalam urutan yang sama seperti yang dideklarasikan dalam file .class, meskipun perilaku tersebut juga tidak ditentukan - Saya telah menulis perpustakaan untuk benar-benar menyortirnya berdasarkan nomor barisnya).

Jadi menegakkan metode tersebut menjadi statis adalah satu-satunya solusi yang masuk akal.

Berikut ini contohnya:

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

Yang mencetak:

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

Seperti yang Anda lihat, setiap pengujian dijalankan dengan instance-nya sendiri. Apa yang dilakukan JUnit pada dasarnya sama dengan ini:

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();
Esko Luontola
sumber
1
"Jika tidak, pada instance kelas pengujian manakah metode yang harus dipanggil?" - Pada contoh pengujian yang dibuat oleh pengujian JUnit yang dijalankan untuk menjalankan pengujian.
HDave
1
Dalam contoh itu, itu membuat tiga contoh uji. Tidak ada yang contoh uji.
Esko Luontola
Ya - saya melewatkannya dalam contoh Anda. Saya memikirkan lebih banyak tentang kapan JUnit dipanggil dari pengujian yang dijalankan ala Eclipse, atau Spring Test, atau Maven. Dalam kasus tersebut, ada satu instance dari kelas pengujian yang dibuat.
HDave
Tidak, JUnit selalu membuat banyak instance kelas pengujian, terlepas dari apa yang kami gunakan untuk meluncurkan pengujian. Hanya jika Anda memiliki Runner kustom untuk kelas pengujian, sesuatu yang berbeda dapat terjadi.
Esko Luontola
Meskipun saya memahami keputusan desain, saya pikir itu tidak memperhitungkan kebutuhan bisnis pengguna. Jadi pada akhirnya keputusan desain internal (yang seharusnya saya tidak terlalu peduli sebagai pengguna begitu lib berfungsi dengan baik) memaksa saya pilihan desain dalam pengujian saya yang merupakan praktik yang sangat buruk. Itu sama sekali tidak gesit: D
gicappa
43

Jawaban singkatnya adalah ini: tidak ada alasan yang tepat untuk itu menjadi statis.

Faktanya, membuatnya statis menyebabkan semua jenis masalah jika Anda menggunakan Junit untuk menjalankan tes integrasi DAO berbasis DBUnit. Persyaratan statis mengganggu injeksi dependensi, akses konteks aplikasi, penanganan resource, logging, dan apa pun yang bergantung pada "getClass".

HDave
sumber
4
Saya menulis superclass kasus uji saya sendiri dan menggunakan anotasi Spring @PostConstructuntuk penyiapan dan @AfterClassuntuk meruntuhkan dan saya mengabaikan yang statis dari Junit sama sekali. Untuk tes DAO saya kemudian menulis TestCaseDataLoaderkelas saya sendiri yang saya panggil dari metode ini.
HDave
9
Itu jawaban yang buruk, jelas ada alasan untuk itu menjadi statis seperti yang ditunjukkan dengan jelas oleh jawaban yang diterima. Anda mungkin tidak setuju dengan keputusan desain, tetapi itu jauh dari menyiratkan bahwa "tidak ada alasan yang baik" untuk keputusan tersebut.
Adam Parkin
8
Tentu saja penulis JUnit punya alasan, saya katakan itu bukan alasan yang bagus ... jadi sumber OP (dan 44 orang lainnya) menjadi bingung. Akan sangat mudah untuk menggunakan metode instance dan meminta pelari pengujian menggunakan konvensi untuk memanggilnya. Pada akhirnya, itulah yang dilakukan semua orang untuk mengatasi batasan ini - gulirkan pelari Anda sendiri, atau putar kelas pengujian Anda sendiri.
HDave
1
@HDave, saya pikir solusi Anda dengan @PostConstructdan @AfterClasshanya berperilaku sama seperti @Beforedan @After. Faktanya, metode Anda akan dipanggil untuk setiap metode pengujian dan tidak hanya sekali untuk seluruh kelas (seperti yang dinyatakan Esko Luontola dalam jawabannya, sebuah instance kelas dibuat untuk setiap metode pengujian). Saya tidak dapat melihat kegunaan solusi Anda jadi (kecuali jika saya melewatkan sesuatu)
magnum87
1
Sudah berjalan dengan benar selama 5 tahun sekarang, jadi saya pikir solusi saya berhasil.
HDave
13

Dokumentasi JUnit tampaknya langka, tetapi saya akan menebak: mungkin JUnit membuat instance baru dari kelas pengujian Anda sebelum menjalankan setiap kasus pengujian, jadi satu-satunya cara agar status "fixture" Anda tetap ada di seluruh proses adalah membuatnya statis, yang dapat diterapkan dengan memastikan fixtureSetup Anda (metode @BeforeClass) statis.

Blair Conrad
sumber
2
Tidak hanya mungkin, tetapi JUnit pasti membuat contoh baru dari kasus uji. Jadi ini satu-satunya alasan.
guerda
Ini adalah satu-satunya alasan yang mereka miliki, tetapi kenyataannya runner Junit dapat melakukan tugas mengeksekusi metode BeforeTests dan AfterTests seperti yang dilakukan testng.
HDave
Apakah TestNG membuat satu instance dari kelas pengujian dan membagikannya dengan semua pengujian di kelas tersebut? Itu membuatnya lebih rentan terhadap efek samping di antara tes.
Esko Luontola
3

Meskipun ini tidak akan menjawab pertanyaan awal. Ini akan menjawab tindak lanjut yang jelas. Cara membuat aturan yang berfungsi sebelum dan sesudah kelas serta sebelum dan sesudah pengujian.

Untuk mencapai itu, Anda dapat menggunakan pola ini:

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

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

Pada sebelum (Kelas) JPAConnection membuat koneksi sekali setelah (Kelas) menutupnya.

getEntityMangermengembalikan kelas dalam JPAConnectionyang mengimplementasikan EntityManager jpa dan dapat mengakses koneksi di dalam jpaConnection. Pada sebelum (tes) itu memulai transaksi setelah (tes) itu berputar kembali.

Ini tidak aman untuk thread tetapi dapat dibuat sedemikian rupa.

Kode yang dipilih dari 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();
    }
  }
}
MP Korstanje
sumber
2

Tampaknya JUnit membuat instance baru dari kelas pengujian untuk setiap metode pengujian. Coba kode ini

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

Outputnya 0 0 0

Ini berarti bahwa jika metode @BeforeClass tidak statis maka itu harus dijalankan sebelum setiap metode pengujian dan tidak akan ada cara untuk membedakan antara semantik @Before dan @BeforeClass

randomuser
sumber
Ini tidak hanya tampak seperti itu, itu adalah seperti itu. Pertanyaan tersebut telah ditanyakan selama bertahun-tahun, berikut jawabannya: martinfowler.com/bliki/JunitNewInstance.html
Paul
1

ada dua jenis anotasi:

  • @BeforeClass (@AfterClass) dipanggil sekali per kelas pengujian
  • @Before (dan @After) dipanggil sebelum setiap pengujian

jadi @BeforeClass harus dideklarasikan statis karena dipanggil sekali. Anda juga harus mempertimbangkan bahwa menjadi statis adalah satu-satunya cara untuk memastikan propagasi "status" yang tepat di antara pengujian (model JUnit memberlakukan satu instance pengujian per @Test) dan, karena di Java hanya metode statis yang dapat mengakses data statis ... @BeforeClass dan @ AfterClass hanya dapat diterapkan ke metode statis.

Tes contoh ini harus menjelaskan penggunaan @BeforeClass vs @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");
    }
}

keluaran:

------------- Output Standar ---------------
sebelum kelas
sebelum
tes 1
setelah
sebelum
tes 2
setelah
setelah kelas
------------- ---------------- ---------------
dfa
sumber
19
Saya menemukan jawaban Anda tidak relevan. Saya tahu semantik BeforeClass dan Before. Ini tidak menjelaskan mengapa harus statis ...
ripper234
1
"Ini memaksa semua init saya untuk menjadi anggota statis, untuk alasan yang tidak baik sejauh yang saya lihat." Jawaban saya harus menunjukkan bahwa init Anda juga bisa non-statis menggunakan @Before, daripada @BeforeClass
dfa
2
Saya ingin melakukan beberapa init satu kali saja, di awal kelas, tetapi pada variabel non-statis.
ripper234
Anda tidak bisa dengan JUnit, maaf. Anda harus menggunakan variabel statis, tidak mungkin.
dfa
1
Jika inisialisasi mahal, Anda dapat menyimpan variabel status untuk merekam apakah Anda telah melakukan init, dan (periksa dan opsional) lakukan init dalam metode @Before ...
Blair Conrad
0

Sesuai JUnit 5, tampaknya filosofi untuk secara ketat membuat instance baru per metode pengujian agak longgar. Mereka telah menambahkan anotasi yang akan membuat instance kelas pengujian hanya sekali. Karenanya, anotasi ini juga memungkinkan metode yang dianotasi dengan @ BeforeAll / @ AfterAll (pengganti ke @ BeforeClass / @ AfterClass) menjadi non-statis. Jadi, kelas uji coba seperti ini:

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

akan mencetak:

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

Jadi, Anda sebenarnya dapat membuat instance objek satu kali per kelas pengujian. Tentu saja, hal ini menjadi tanggung jawab Anda sendiri untuk menghindari mutasi objek yang dibuat dengan cara ini.

EJJ
sumber
-11

Untuk mengatasi masalah ini, ubah saja metodenya

public void setUpBeforeClass 

untuk

public static void setUpBeforeClass()

dan semua yang didefinisikan dalam metode ini untuk static.

sri
sumber
2
Ini sama sekali tidak menjawab pertanyaan itu.
rgargente