Praktik Terbaik: Inisialisasi bidang kelas JUnit di setUp () atau di deklarasi?

120

Haruskah saya menginisialisasi bidang kelas pada deklarasi seperti ini?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Atau di setUp () seperti ini?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Saya cenderung menggunakan formulir pertama karena lebih ringkas, dan memungkinkan saya menggunakan bidang akhir. Jika saya tidak perlu menggunakan metode setUp () untuk penyiapan, haruskah saya tetap menggunakannya, dan mengapa?

Klarifikasi: JUnit akan membuat instance kelas pengujian satu kali per metode pengujian. Artinya listakan dibuat sekali per pengujian, di mana pun saya mendeklarasikannya. Ini juga berarti tidak ada ketergantungan temporal di antara pengujian. Jadi sepertinya tidak ada keuntungan menggunakan setUp (). Namun FAQ JUnit memiliki banyak contoh yang menginisialisasi koleksi kosong di setUp (), jadi saya pikir pasti ada alasannya.

Craig P. Motlin
sumber
2
Berhati-hatilah karena jawabannya berbeda di JUnit 4 (inisialisasi dalam deklarasi) dan JUnit 3 (gunakan setUp); inilah akar dari kebingungan.
Nils von Barth
Lihat juga stackoverflow.com/questions/6094081/…
Grigory Kislin

Jawaban:

99

Jika Anda bertanya-tanya secara khusus tentang contoh di FAQ JUnit, seperti template pengujian dasar , saya pikir praktik terbaik yang ditunjukkan di sana adalah bahwa kelas yang diuji harus dibuat dalam metode penyiapan Anda (atau dalam metode pengujian) .

Ketika contoh JUnit membuat ArrayList dalam metode setUp, mereka semua melanjutkan untuk menguji perilaku ArrayList tersebut, dengan kasus seperti testIndexOutOfBoundException, testEmptyCollection, dan sejenisnya. Ada perspektif seseorang yang menulis kelas dan memastikannya berfungsi dengan benar.

Anda mungkin harus melakukan hal yang sama saat menguji kelas Anda sendiri: buat objek Anda di setUp atau dalam metode pengujian, sehingga Anda bisa mendapatkan keluaran yang wajar jika Anda memecahnya nanti.

Di sisi lain, jika Anda menggunakan kelas koleksi Java (atau kelas perpustakaan lain, dalam hal ini) dalam kode pengujian Anda, itu mungkin bukan karena Anda ingin mengujinya - itu hanya bagian dari perlengkapan pengujian. Dalam kasus ini, Anda dapat dengan aman menganggapnya berfungsi sebagaimana mestinya, jadi memulainya dalam deklarasi tidak akan menjadi masalah.

Untuk apa nilainya, saya bekerja pada basis kode yang cukup besar, berusia beberapa tahun, yang dikembangkan TDD. Kami biasanya menginisialisasi hal-hal dalam deklarasi mereka dalam kode pengujian, dan selama satu setengah tahun saya berada di proyek ini, itu tidak pernah menimbulkan masalah. Jadi setidaknya ada beberapa bukti anekdot bahwa hal itu masuk akal untuk dilakukan.

Lumut Collum
sumber
45

Saya mulai menggali sendiri dan saya menemukan satu keuntungan potensial dari penggunaan setUp(). Jika ada pengecualian yang dilontarkan selama eksekusi setUp(), JUnit akan mencetak pelacakan tumpukan yang sangat membantu. Di sisi lain, jika pengecualian dilemparkan selama konstruksi objek, pesan kesalahan hanya mengatakan JUnit tidak dapat membuat contoh kasus uji dan Anda tidak melihat nomor baris tempat kegagalan terjadi, mungkin karena JUnit menggunakan refleksi untuk membuat contoh pengujian kelas.

Semua ini tidak berlaku untuk contoh membuat koleksi kosong, karena itu tidak akan pernah terlempar, tetapi ini merupakan keuntungan dari setUp()metode ini.

Craig P. Motlin
sumber
18

Selain jawaban Alex B.

Bahkan diperlukan untuk menggunakan metode setUp untuk membuat instance resource dalam keadaan tertentu. Melakukan ini di konstruktor tidak hanya masalah pengaturan waktu, tetapi karena cara JUnit menjalankan pengujian, setiap status pengujian akan dihapus setelah menjalankan satu pengujian.

JUnit pertama-tama membuat instance testClass untuk setiap metode pengujian dan mulai menjalankan pengujian setelah setiap instance dibuat. Sebelum menjalankan metode pengujian, metode penyiapannya dijalankan, di mana beberapa status dapat disiapkan.

Jika status database akan dibuat di konstruktor, semua instance akan membuat instance status db tepat setelah satu sama lain, sebelum menjalankan setiap pengujian. Pada pengujian kedua, pengujian akan berjalan dengan keadaan kotor.

Siklus hidup JUnits:

  1. Buat instance kelas uji yang berbeda untuk setiap metode pengujian
  2. Ulangi untuk setiap instance kelas uji: panggil penyiapan + panggil metode uji

Dengan beberapa logging dalam pengujian dengan dua metode pengujian yang Anda dapatkan: (nomor adalah kode hash)

  • Membuat instance baru: 5718203
  • Membuat instance baru: 5947506
  • Penyiapan: 5718203
  • TestOne: 5718203
  • Penyiapan: 5947506
  • TestTwo: 5947506
Jurgen Hannaert
sumber
3
Benar, tapi di luar topik. Basis data pada dasarnya adalah keadaan global. Ini bukan masalah yang saya hadapi. Saya hanya peduli dengan kecepatan eksekusi tes independen yang benar.
Craig P. Motlin
Urutan inisialisasi ini hanya berlaku di JUnit 3, yang merupakan peringatan penting. Dalam JUnit 4, instance pengujian dibuat dengan malas, jadi inisialisasi dalam deklarasi atau dalam metode penyiapan keduanya terjadi pada waktu pengujian. Juga untuk penyiapan satu kali, yang dapat digunakan @BeforeClassdi JUnit 4.
Nils von Barth
11

Di JUnit 4:

  • Untuk Class Under Test , inisialisasi dalam @Beforemetode, untuk menangkap kegagalan.
  • Untuk kelas lain , lakukan inisialisasi di deklarasi ...
    • ... untuk singkatnya, dan untuk menandai bidang final, persis seperti yang dinyatakan dalam pertanyaan,
    • ... kecuali inisialisasi kompleks yang bisa gagal, dalam hal ini digunakan @Before, untuk menangkap kegagalan.
  • Untuk status global (khususnya inisialisasi lambat , seperti database), gunakan @BeforeClass, tapi hati-hati dengan dependensi antar pengujian.
  • Inisialisasi objek yang digunakan dalam pengujian tunggal tentunya harus dilakukan dalam metode pengujian itu sendiri.

Menginisialisasi dalam @Beforemetode atau metode pengujian memungkinkan Anda mendapatkan pelaporan kesalahan yang lebih baik tentang kegagalan. Ini sangat berguna untuk membuat instance Class Under Test (yang mungkin rusak), tetapi juga berguna untuk memanggil sistem eksternal, seperti akses sistem file ("file tidak ditemukan") atau menghubungkan ke database ("koneksi ditolak").

Dapat diterima untuk memiliki standar yang sederhana dan selalu menggunakan @Before(kesalahan yang jelas tetapi bertele-tele) atau selalu diinisialisasi dalam deklarasi (ringkas tetapi memberikan kesalahan yang membingungkan), karena aturan pengkodean yang rumit sulit diikuti, dan ini bukan masalah besar.

Inisialisasi in setUpadalah peninggalan JUnit 3, di mana semua instance pengujian diinisialisasi dengan penuh semangat, yang menyebabkan masalah (kecepatan, memori, kehabisan resource) jika Anda melakukan inisialisasi yang mahal. Oleh karena itu, praktik terbaiknya adalah melakukan inisialisasi mahal setUp, yang hanya dijalankan saat pengujian dijalankan. Ini tidak lagi berlaku, jadi penggunaannya jauh lebih sedikit setUp.

Ini merangkum beberapa jawaban lain yang mengubur lede, terutama oleh Craig P. Motlin (pertanyaan itu sendiri dan jawaban sendiri), Moss Collum (kelas yang diuji), dan dsaff.

Nils von Barth
sumber
7

Di JUnit 3, penginisialisasi bidang Anda akan dijalankan satu kali per metode pengujian sebelum pengujian apa pun dijalankan . Selama nilai kolom Anda kecil dalam memori, memerlukan sedikit waktu penyiapan, dan tidak memengaruhi status global, penggunaan penginisialisasi kolom secara teknis baik-baik saja. Namun, jika tidak ada, Anda mungkin akan menghabiskan banyak memori atau waktu menyiapkan bidang Anda sebelum pengujian pertama dijalankan, dan bahkan mungkin kehabisan memori. Karena alasan ini, banyak pengembang selalu menetapkan nilai bidang dalam metode setUp (), yang selalu aman, meskipun tidak benar-benar diperlukan.

Perhatikan bahwa di JUnit 4, inisialisasi objek pengujian terjadi tepat sebelum pengujian dijalankan, sehingga penggunaan inisialisasi bidang lebih aman, dan merupakan gaya yang direkomendasikan.

dsaff
sumber
Menarik. Jadi perilaku yang Anda jelaskan pada awalnya hanya berlaku untuk JUnit 3?
Craig P. Motlin
6

Dalam kasus Anda (membuat daftar) tidak ada perbedaan dalam praktiknya. Tapi umumnya lebih baik menggunakan setUp (), karena itu akan membantu Junit melaporkan Pengecualian dengan benar. Jika pengecualian terjadi di konstruktor / penginisialisasi Tes, itu adalah kegagalan uji . Namun, jika pengecualian terjadi selama penyiapan, wajar untuk menganggapnya sebagai masalah dalam menyiapkan pengujian, dan junit melaporkannya dengan tepat.

amit
sumber
1
kata yang bagus. Biasakan saja untuk selalu membuat instance di setUp () dan Anda memiliki satu pertanyaan yang tidak perlu dikhawatirkan - misalnya di mana saya harus membuat instance fooBar saya, di mana koleksi saya. Ini semacam standar pengkodean yang hanya perlu Anda patuhi. Manfaat Anda bukan dengan daftar, tetapi dengan contoh lain.
Olaf Kock
@Olaf Terima kasih atas info tentang standar pengkodean, saya belum memikirkannya. Saya cenderung lebih setuju dengan gagasan Moss Collum tentang standar pengkodean.
Craig P. Motlin
5

Saya lebih suka keterbacaan pertama yang paling sering tidak menggunakan metode pengaturan. Saya membuat pengecualian ketika operasi penyetelan dasar membutuhkan waktu lama dan diulangi dalam setiap pengujian.
Pada titik itu saya memindahkan fungsionalitas itu ke dalam metode penyiapan menggunakan @BeforeClassanotasi (optimalkan nanti).

Contoh pengoptimalan menggunakan @BeforeClassmetode penyiapan: Saya menggunakan dbunit untuk beberapa tes fungsional database. Metode pengaturan bertanggung jawab untuk meletakkan database dalam keadaan yang diketahui (sangat lambat ... 30 detik - 2 menit tergantung pada jumlah data). Saya memuat data ini dalam metode penyiapan yang dianotasi dengan @BeforeClassdan kemudian menjalankan 10-20 tes terhadap kumpulan data yang sama sebagai lawan dari memuat ulang / menginisialisasi database di dalam setiap tes.

Menggunakan Junit 3.8 (memperluas TestCase seperti yang ditunjukkan dalam contoh Anda) memerlukan penulisan kode lebih dari sekadar menambahkan anotasi, tetapi "jalankan sekali sebelum penyiapan kelas" masih memungkinkan.

Alex B
sumber
1
+1 karena saya juga lebih suka keterbacaan. Namun, saya tidak yakin bahwa cara kedua adalah optimasi sama sekali.
Craig P. Motlin
@Motlin Saya menambahkan contoh dbunit untuk menjelaskan bagaimana Anda dapat mengoptimalkan dengan penyiapan.
Alex B
Basis data pada dasarnya adalah keadaan global. Jadi, memindahkan penyiapan db ke setUp () bukanlah pengoptimalan, pengujian harus diselesaikan dengan benar.
Craig P. Motlin
@ Alex B: Seperti yang dikatakan Motlin, ini bukan pengoptimalan. Anda hanya mengubah di mana dalam kode inisialisasi dilakukan, tetapi tidak berapa kali atau seberapa cepat.
Eddie
Saya bermaksud untuk menyiratkan penggunaan anotasi "@BeforeClass". Mengedit contoh untuk memperjelas.
Alex B
2

Karena setiap pengujian dijalankan secara independen, dengan instance baru dari objek, tidak banyak gunanya objek Test memiliki status internal apa pun kecuali yang dibagi antara setUp()dan pengujian individual dan tearDown(). Ini adalah salah satu alasan (selain alasan yang diberikan orang lain) bahwa metode ini baik untuk digunakan setUp().

Catatan: Merupakan ide yang buruk jika objek pengujian JUnit mempertahankan status statis! Jika Anda menggunakan variabel statis dalam pengujian Anda untuk apa pun selain pelacakan atau tujuan diagnostik, Anda membatalkan sebagian tujuan JUnit, yaitu bahwa pengujian dapat (dan mungkin) dijalankan dalam urutan apa pun, setiap pengujian berjalan dengan segar, keadaan bersih.

Keuntungan menggunakan setUp()adalah Anda tidak perlu memotong-dan-menempelkan kode inisialisasi di setiap metode pengujian dan Anda tidak memiliki kode penyiapan pengujian di konstruktor. Dalam kasus Anda, ada sedikit perbedaan. Hanya membuat daftar kosong dapat dilakukan dengan aman saat Anda menampilkannya atau dalam konstruktor karena ini adalah inisialisasi yang sepele. Namun, seperti yang Anda dan orang lain tunjukkan, apa pun yang mungkin bisa menimbulkan Exceptionharus dilakukan setUp()sehingga Anda mendapatkan tumpukan tumpukan diagnostik jika gagal.

Dalam kasus Anda, di mana Anda hanya membuat daftar kosong, saya akan melakukan cara yang sama seperti yang Anda sarankan: Tetapkan daftar baru pada titik deklarasi. Terutama karena dengan cara ini Anda memiliki opsi untuk menandainya finaljika ini masuk akal untuk kelas pengujian Anda.

Eddie
sumber
1
+1 karena Anda adalah orang pertama yang benar-benar mendukung inisialisasi daftar selama konstruksi objek untuk menandainya sebagai final. Hal-hal tentang variabel statis tidak sesuai dengan topik pertanyaan.
Craig P. Motlin
@Motlin: benar, hal-hal tentang variabel statis sedikit di luar topik. Saya tidak yakin mengapa saya menambahkannya, tetapi sepertinya tepat pada saat itu, perpanjangan dari apa yang saya katakan di paragraf pertama.
Eddie
Keuntungan dari finaldisebutkan dalam pertanyaan.
Nils von Barth
0
  • Nilai konstanta (penggunaan dalam perlengkapan atau pernyataan) harus diinisialisasi dalam deklarasinya dan final(tidak pernah berubah)

  • objek yang diuji harus diinisialisasi dalam metode penyiapan karena kita dapat mengatur semuanya. Tentu saja kami tidak dapat mengatur sesuatu sekarang tetapi kami dapat mengaturnya nanti. Instansiasi dalam metode init akan memudahkan perubahan.

  • dependensi objek yang diuji jika ini diolok-olok, bahkan tidak boleh dibuat sendiri: hari ini kerangka kerja tiruan dapat membuat instance dengan refleksi.

Pengujian tanpa ketergantungan pada tiruan akan terlihat seperti:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Pengujian dengan dependensi untuk diisolasi bisa terlihat seperti:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
davidxxx
sumber