Dalam TDD terdapat 3 fase yaitu menyusun, bertindak dan menegaskan (diberikan, kapan, lalu dalam BDD). Fase penegasan memiliki dukungan alat yang hebat, Anda mungkin familiar dengan AssertJ, FEST-Assert, atau Hamcrest. Berbeda dengan fase pengaturan. Meskipun menyusun data pengujian sering kali merupakan tantangan dan sebagian besar pengujian biasanya dikhususkan untuk hal tersebut, sulit untuk menunjukkan alat yang mendukungnya.
Test Arranger mencoba mengisi kesenjangan ini dengan mengatur instance kelas yang diperlukan untuk pengujian. Instance diisi dengan nilai pseudo-acak yang menyederhanakan proses pembuatan data pengujian. Penguji hanya mendeklarasikan tipe objek yang diperlukan dan mendapatkan instance baru. Jika nilai pseudo-acak untuk kolom tertentu tidak cukup baik, hanya kolom ini yang harus disetel secara manual:
Product product = Arranger . some ( Product . class );
product . setBrand ( "Ocado" );
< dependency >
< groupId >com.ocadotechnology.gembus groupId >
< artifactId >test-arranger artifactId >
< version >1.6.3 version >
dependency >
testImplementation ' com.ocadotechnology.gembus:test-arranger:1.6.3 '
Kelas Arranger memiliki beberapa metode statis untuk menghasilkan nilai pseudo-acak bertipe sederhana. Masing-masing memiliki fungsi wraping untuk mempermudah panggilan di Kotlin. Beberapa kemungkinan panggilan tercantum di bawah ini:
Jawa | Kotlin | hasil |
---|---|---|
Arranger.some(Product.class) | some | contoh Produk dengan semua bidang diisi dengan nilai |
Arranger.some(Product.class, "brand") | some | contoh Produk tanpa nilai untuk bidang merek |
Arranger.someSimplified(Category.class) | someSimplified | sebuah instance dari Kategori, bidang kumpulan tipe memiliki ukuran yang dikurangi menjadi 1 dan kedalaman pohon objek dibatasi hingga 3 |
Arranger.someObjects(Product.class, 7) | someObjects | aliran ukuran 7 dari contoh Produk |
Arranger.someEmail() | someEmail() | string yang berisi alamat email |
Arranger.someLong() | someLong() | nomor acak semu bertipe panjang |
Arranger.someFrom(listOfCategories) | someFrom(listOfCategories) | entri dari listOfCategories |
Arranger.someText() | someText() | string yang dihasilkan dari Rantai Markov; secara default, ini adalah rantai yang sangat sederhana, tetapi dapat dikonfigurasi ulang dengan meletakkan file 'enMarkovChain' lainnya di jalur kelas pengujian dengan definisi alternatif, Anda dapat menemukan file yang dilatih tentang korpus bahasa Inggris di sini; lihat file 'enMarkovChain' yang disertakan dalam proyek untuk format file |
- | some | contoh Produk dengan semua bidang diisi dengan nilai acak kecuali name yang disetel ke "tidak terlalu acak", sintaksis ini dapat digunakan untuk menyetel bidang objek sebanyak yang diperlukan, tetapi masing-masing objek harus bisa berubah |
Data yang benar-benar acak mungkin tidak cocok untuk setiap kasus uji. Seringkali ada setidaknya satu bidang yang penting untuk tujuan pengujian dan memerlukan nilai tertentu. Jika kelas yang diatur dapat diubah, atau merupakan kelas data Kotlin, atau ada cara untuk membuat salinan yang diubah (misalnya @Builder(toBuilder = true)) Lombok, gunakan saja apa yang tersedia. Untungnya, meskipun tidak dapat disesuaikan, Anda dapat menggunakan Test Arranger. Ada versi khusus dari metode some()
dan someObjects()
yang menerima parameter tipe Map
. Kunci dalam peta ini mewakili nama bidang sementara pemasok terkait memberikan nilai yang akan ditetapkan oleh Test Arranger untuk Anda pada bidang tersebut, misalnya:
Product product = Arranger . some ( Product . class , Map . of ( "name" , () -> value ));
Secara default, nilai acak dihasilkan sesuai dengan jenis bidang. Nilai acak tidak selalu sesuai dengan invarian kelas. Ketika suatu entitas selalu perlu diatur mengenai beberapa aturan mengenai nilai bidang, Anda dapat menyediakan pengatur khusus:
class ProductArranger extends CustomArranger < Product > {
@ Override
protected Product instance () {
Product product = enhancedRandom . nextObject ( Parent . class );
product . setPrice ( BigDecimal . valueOf ( Arranger . somePositiveLong ( 9_999L )));
return product ;
}
}
Untuk memiliki kendali atas proses pembuatan instance Product
kita perlu mengganti metode instance()
. Di dalam metode ini kita dapat membuat instance Product
sesuka kita. Secara khusus, kami dapat menghasilkan beberapa nilai acak. Untuk kenyamanan, kami memiliki bidang enhancedRandom
di kelas CustomArranger
. Dalam contoh yang diberikan, kami membuat instance Product
dengan semua bidang memiliki nilai pseudo-acak, tetapi kemudian kami mengubah harganya menjadi sesuatu yang dapat diterima di domain kami. Itu tidak negatif dan lebih kecil dari angka 10k.
ProductArranger
secara otomatis (menggunakan refleksi) diambil oleh Arranger dan digunakan setiap kali instance Product
baru diminta. Ini tidak hanya berlaku untuk panggilan langsung seperti Arranger.some(Product.class)
, tetapi juga panggilan tidak langsung. Dengan asumsi ada kelas Shop
dengan products
bidang bertipe List
. Saat memanggil Arranger.some(Shop.class)
, arranger akan menggunakan ProductArranger
untuk membuat semua produk yang disimpan di Shop.products
.
Perilaku test-arranger dapat dikonfigurasi menggunakan properti. Jika Anda membuat file arranger.properties
dan menyimpannya di root classpath (biasanya direktori src/test/resources/
), file tersebut akan diambil dan properti berikut akan diterapkan:
arranger.root
Arranger kustom diambil menggunakan refleksi. Semua kelas yang memperluas CustomArranger
dianggap sebagai pengatur khusus. Refleksinya difokuskan pada paket tertentu yang secara default adalah com.ocado
. Itu belum tentu nyaman bagi Anda. Namun, dengan arranger.root=your_package
dapat diubah menjadi your_package
. Cobalah untuk membuat paket sespesifik mungkin karena memiliki sesuatu yang generik (misalnya hanya com
yang merupakan paket root di banyak perpustakaan) akan mengakibatkan pemindaian ratusan kelas yang akan memakan waktu lama.arranger.randomseed
Secara default, seed yang sama selalu digunakan untuk menginisialisasi generator nilai pseudorandom yang mendasarinya. Akibatnya, eksekusi selanjutnya akan menghasilkan nilai yang sama. Untuk mencapai keacakan di seluruh proses, yaitu untuk selalu memulai dengan nilai acak lainnya, pengaturan arranger.randomseed=true
diperlukan.arranger.cache.enable
Proses mengatur instance acak memerlukan beberapa waktu. Jika Anda membuat instance dalam jumlah besar dan tidak membutuhkannya secara acak, mengaktifkan cache mungkin merupakan cara yang tepat. Saat diaktifkan, cache menyimpan referensi ke setiap instance acak dan pada titik tertentu test-arranger berhenti membuat instance baru dan malah menggunakan kembali instance yang di-cache. Secara default, cache dinonaktifkan.arranger.overridedefaults
Test-arranger menghormati inisialisasi field default, yaitu ketika ada field yang diinisialisasi dengan string kosong, instance yang dikembalikan oleh test-arranger memiliki string kosong di field ini. Tidak selalu itu yang Anda perlukan dalam pengujian, terutama ketika ada konvensi dalam proyek untuk menginisialisasi bidang dengan nilai kosong. Untungnya, Anda dapat memaksa test-arranger untuk menimpa nilai default dengan nilai acak. Setel arranger.overridedefaults
ke true untuk mengganti inisialisasi default.arranger.maxRandomizationDepth
Beberapa struktur data pengujian dapat menghasilkan rantai panjang objek apa pun yang saling mereferensikan. Namun, untuk menggunakannya secara efektif dalam uji kasus, penting untuk mengontrol panjang rantai tersebut. Secara default, Test-arranger berhenti membuat objek baru pada kedalaman sarang tingkat ke-4. Jika pengaturan default ini tidak sesuai dengan kasus uji proyek Anda, maka dapat disesuaikan menggunakan parameter ini. Ketika Anda memiliki catatan Java yang dapat digunakan sebagai data pengujian, namun Anda perlu mengubah satu atau dua bidangnya, kelas Data
dengan metode penyalinannya memberikan solusi. Hal ini sangat berguna ketika menangani catatan yang tidak dapat diubah yang tidak memiliki cara yang jelas untuk mengubah bidangnya secara langsung.
Metode Data.copy
memungkinkan Anda membuat salinan dangkal dari suatu rekaman sambil secara selektif memodifikasi bidang yang diinginkan. Dengan menyediakan peta penggantian bidang, Anda dapat menentukan bidang yang perlu diubah dan nilai barunya. Metode salin menangani pembuatan instance rekaman baru dengan nilai bidang yang diperbarui.
Pendekatan ini menyelamatkan Anda dari pembuatan objek rekaman baru secara manual dan mengatur bidang satu per satu, sehingga memberikan cara mudah untuk menghasilkan data pengujian dengan sedikit variasi dari rekaman yang sudah ada.
Secara keseluruhan, kelas Data dan metode penyalinannya menyelamatkan situasi dengan memungkinkan pembuatan salinan rekaman dangkal dengan kolom yang dipilih diubah, memberikan fleksibilitas dan kenyamanan saat bekerja dengan tipe rekaman yang tidak dapat diubah:
Data . copy ( myRecord , Map . of ( "recordFieldName" , () -> "altered value" ));
Saat menjalani pengujian suatu proyek perangkat lunak, orang jarang mendapat kesan bahwa proyek tersebut tidak dapat dilakukan dengan lebih baik. Dalam lingkup penyusunan data pengujian, ada dua area yang kami coba tingkatkan dengan Test Arranger.
Tes akan lebih mudah dipahami ketika mengetahui maksud dari pembuatnya, yaitu mengapa tes tersebut ditulis dan masalah apa yang harus dideteksi. Sayangnya, bukanlah hal yang luar biasa untuk melihat pengujian memiliki pernyataan di bagian susunan (yang diberikan) seperti berikut:
Product product = Product . builder ()
. withName ( "Some name" )
. withBrand ( "Some brand" )
. withPrice ( new BigDecimal ( "12.99" ))
. withCategory ( "Water, Juice & Drinks / Juice / Fresh" )
...
. build ();
Saat melihat kode seperti itu, sulit untuk mengatakan nilai mana yang relevan untuk pengujian dan mana yang disediakan hanya untuk memenuhi beberapa persyaratan bukan nol. Kalau tesnya soal merek, kenapa tidak ditulis seperti itu:
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
Sekarang jelas bahwa merek itu penting. Mari kita coba melangkah lebih jauh. Keseluruhan tes mungkin terlihat sebagai berikut:
//arrange
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( "Some brand" )
Sekarang kami sedang menguji apakah laporan tersebut dibuat untuk merek "Beberapa merek". Tapi apakah itu tujuannya? Lebih masuk akal untuk mengharapkan bahwa laporan akan dibuat untuk merek yang sama, tempat produk tertentu ditugaskan. Jadi yang ingin kami uji adalah:
//arrange
Product product = Arranger . some ( Product . class );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( product . getBrand ())
Jika bidang merek dapat diubah dan kami khawatir sut
akan mengubahnya, kami dapat menyimpan nilainya dalam variabel sebelum masuk ke fase tindakan dan kemudian menggunakannya untuk pernyataan. Ujiannya akan lebih lama, tapi niatnya tetap jelas.
Patut dicatat bahwa apa yang baru saja kita lakukan adalah penerapan pola Nilai yang Dihasilkan dan Metode Penciptaan sampai batas tertentu yang dijelaskan dalam Pola Tes xUnit: Kode Tes Refactoring oleh Gerard Meszaros.
Pernahkah Anda mengubah satu hal kecil dalam kode produksi dan berakhir dengan kesalahan dalam lusinan pengujian? Beberapa dari mereka melaporkan pernyataan yang gagal, beberapa bahkan mungkin menolak untuk dikompilasi. Ini adalah bau kode operasi senapan yang baru saja ditembakkan pada tes Anda yang tidak bersalah. Yah, mungkin tidak terlalu polos karena bisa dirancang secara berbeda, untuk membatasi kerusakan tambahan yang disebabkan oleh perubahan kecil. Mari kita menganalisisnya menggunakan sebuah contoh. Misalkan kita memiliki kelas berikut di domain kita:
class TimeRange {
private LocalDateTime start ;
private long durationinMs ;
public TimeRange ( LocalDateTime start , long durationInMs ) {
...
dan itu digunakan di banyak tempat. Khususnya dalam pengujian, tanpa Test Arranger, menggunakan pernyataan seperti ini: new TimeRange(LocalDateTime.now(), 3600_000L);
Apa jadinya jika karena beberapa alasan penting kita terpaksa mengubah kelas menjadi:
class TimeRange {
private LocalDateTime start ;
private LocalDateTime end ;
public TimeRange ( LocalDateTime start , LocalDateTime end ) {
...
Cukup menantang untuk menghasilkan serangkaian pemfaktoran ulang yang mengubah versi lama ke versi baru tanpa merusak semua pengujian yang bergantung. Kemungkinan besar adalah skenario di mana pengujian disesuaikan dengan API baru dari kelas tersebut satu per satu. Ini berarti banyak pekerjaan yang tidak terlalu menarik dengan banyak pertanyaan mengenai nilai durasi yang diinginkan (haruskah saya mengonversinya dengan hati-hati ke end
tipe LocalDateTime atau hanya nilai acak yang sesuai). Hidup akan lebih mudah dengan Test Arranger. Ketika di semua tempat yang tidak memerlukan null TimeRange
kita memiliki Arranger.some(TimeRange.class)
, TimeRange
versi baru sama bagusnya dengan versi lama. Hal ini menyisakan beberapa kasus yang tidak memerlukan TimeRange
acak, namun karena kita telah menggunakan Test Arranger untuk mengungkapkan tujuan pengujian, dalam setiap kasus, kita tahu persis nilai apa yang harus digunakan untuk TimeRange
.
Namun, bukan hanya itu yang bisa kami lakukan untuk meningkatkan pengujian. Agaknya, kita dapat mengidentifikasi beberapa kategori instance TimeRange
, misalnya rentang dari masa lalu, rentang dari masa depan, dan rentang yang sedang aktif. TimeRangeArranger
adalah tempat yang bagus untuk mengaturnya:
class TimeRangeArranger extends CustomArranger < TimeRange > {
private final long MAX_DISTANCE = 999_999L ;
@ Override
protected TimeRange instance () {
LocalDateTime start = enhancedRandom . nextObject ( LocalDateTime . class );
LocalDateTime end = start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
public TimeRange fromPast () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime end = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( end . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )), end );
}
public TimeRange fromFuture () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )));
}
public TimeRange currentlyActive () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
LocalDateTime end = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
}
Metode pembuatan seperti itu tidak boleh dibuat terlebih dahulu melainkan disesuaikan dengan kasus uji yang ada. Meskipun demikian, ada kemungkinan TimeRangeArranger
akan mencakup semua kasus di mana instance TimeRange
dibuat untuk pengujian. Sebagai konsekuensinya, sebagai pengganti panggilan konstruktor dengan beberapa parameter misterius, kami memiliki pengatur dengan metode terkenal yang menjelaskan makna domain dari objek yang dibuat dan membantu memahami maksud pengujian.
Kami mengidentifikasi dua tingkat pembuat data pengujian saat mendiskusikan tantangan yang diselesaikan oleh Test Arranger. Untuk melengkapi gambarannya kita perlu menyebutkan setidaknya satu lagi, yaitu Jadwal. Demi diskusi ini, kita dapat berasumsi bahwa Fixture adalah kelas yang dirancang untuk membuat struktur data pengujian yang kompleks. Penata kustom selalu terfokus pada satu kelas, namun terkadang Anda dapat mengamati dalam kasus pengujian Anda konstelasi dua kelas atau lebih yang berulang. Itu mungkin Pengguna dan rekening Banknya. Mungkin ada CustomArranger untuk masing-masingnya, tapi mengapa mengabaikan fakta bahwa mereka sering berkumpul. Inilah saatnya kita harus mulai memikirkan sebuah Fixture. Ini akan bertanggung jawab untuk membuat rekening Pengguna dan Bank (mungkin menggunakan pengatur khusus khusus) dan menghubungkan keduanya. Perlengkapan dijelaskan secara rinci, termasuk beberapa varian implementasi dalam xUnit Test Patterns: Refactoring Test Code oleh Gerard Meszaros.
Jadi kami memiliki tiga jenis blok penyusun di kelas pengujian. Masing-masing dapat dianggap sebagai mitra konsep (blok penyusun Desain Berbasis Domain) dari kode produksi:
Di permukaan terdapat benda-benda primitif dan sederhana. Itu adalah sesuatu yang muncul bahkan dalam pengujian unit yang paling sederhana. Anda dapat mengatur data pengujian tersebut dengan metode someXxx
dari kelas Arranger
.
Jadi, Anda mungkin memiliki layanan yang memerlukan pengujian yang hanya berfungsi pada instance User
atau kelas User
dan kelas lain yang terdapat dalam kelas User
, seperti daftar alamat. Untuk menangani kasus seperti ini, biasanya diperlukan pengatur khusus, yaitu UserArranger
. Ini akan membuat instance User
menghormati semua batasan dan invarian kelas. Selain itu, ia akan mengambil AddressArranger
, jika ada, untuk mengisi daftar alamat dengan data yang valid. Ketika beberapa kasus uji memerlukan tipe pengguna tertentu, misalnya pengguna tunawisma dengan daftar alamat kosong, metode tambahan dapat dibuat di UserArranger. Sebagai konsekuensinya, setiap kali diperlukan untuk membuat instance User
untuk pengujian, cukup dengan melihat UserArranger
dan memilih metode pabrik yang memadai atau cukup panggil Arranger.some(User.class)
.
Kasus yang paling menantang adalah pengujian yang bergantung pada struktur data yang besar. Di eCommerce, itu bisa berupa toko yang berisi banyak produk, tetapi juga akun pengguna dengan riwayat belanja. Menyusun data untuk kasus uji semacam itu biasanya tidak sepele dan mengulangi hal seperti itu bukanlah tindakan yang bijaksana. Jauh lebih baik untuk menyimpannya di kelas khusus dengan metode yang dikenal, seperti shopWithNineProductsAndFourCustomers
, dan menggunakannya kembali di setiap pengujian. Kami sangat menyarankan untuk menggunakan konvensi penamaan untuk kelas-kelas tersebut, agar mudah ditemukan, saran kami adalah menggunakan Fixture
postfix. Pada akhirnya, kita mungkin akan mendapatkan sesuatu seperti ini:
class ShopFixture {
Repository repo ;
public void shopWithNineProductsAndFourCustomers () {
Arranger . someObjects ( Product . class , 9 )
. forEach ( p -> repo . save ( p ));
Arranger . someObjects ( Customer . class , 4 )
. forEach ( p -> repo . save ( p ));
}
}
Versi test-arranger terbaru dikompilasi menggunakan Java 17 dan harus digunakan pada runtime Java 17+. Namun, ada juga cabang Java 8 untuk kompatibilitas mundur, yang tercakup dalam versi 1.4.x.