Tidak seperti banyak platform, Java menderita kekurangan perpustakaan koneksi rintisan. Jika Anda telah berada di dunia ini untuk waktu yang lama, maka Anda mungkin harus terbiasa dengan WireMock, Betamax atau bahkan Spock. Banyak pengembang dalam tes menggunakan Mockito untuk menggambarkan perilaku objek, DataJpaTest dengan database h2 lokal, tes Mentimun. Hari ini Anda akan menemukan alternatif ringan yang akan membantu Anda mengatasi berbagai masalah yang mungkin Anda temui menggunakan pendekatan ini. Secara khusus, anyStub mencoba memecahkan masalah berikut:
- menyederhanakan konfigurasi lingkungan pengujian
- mengotomatiskan pengumpulan data untuk pengujian
- tetap menguji aplikasi Anda dan menghindari pengujian untuk hal lain
Apa itu anyStub dan bagaimana cara kerjanya
AnyStub membungkus panggilan fungsi, mencoba menemukan panggilan yang cocok yang telah direkam. Dua hal dapat terjadi dengan ini:
- jika ada panggilan yang cocok, anyStub akan mengembalikan hasil yang direkam terkait dengan panggilan itu dan mengembalikannya
- jika tidak ada panggilan yang cocok dan akses ke sistem eksternal diizinkan, anyStub akan melakukan panggilan ini, merekam hasil ini dan mengembalikannya
Di luar kotak, anyStub menyediakan pembungkus untuk klien http dari Apache HttpClient untuk membuat stubs untuk permintaan http dan beberapa antarmuka dari javax.sql. * Untuk koneksi DB. Anda juga diberikan API untuk membuat stub untuk koneksi lain.
AnyStub adalah pustaka kelas sederhana dan tidak memerlukan konfigurasi khusus dari lingkungan Anda. Pustaka ini ditujukan untuk bekerja dengan aplikasi spring-boot dan Anda akan mendapatkan manfaat maksimal dengan mengikuti jalur ini. Anda dapat menggunakannya di luar Spring, di aplikasi Java biasa, tetapi Anda harus melakukan pekerjaan tambahan. Deskripsi berikut difokuskan pada pengujian aplikasi boot-musim semi.
Mari kita lihat pengujian integrasi. Ini adalah cara paling menarik dan komprehensif untuk menguji sistem Anda. Bahkan, spring-boot dan JUnit melakukan hampir segalanya untuk Anda ketika Anda menulis anotasi magis:
@RunWith(SpringRunner.class) @SpringBootTest
Saat ini, tes integrasi diremehkan dan digunakan sampai batas tertentu, dan beberapa pengembang menghindarinya. Ini terutama disebabkan oleh persiapan dan pemeliharaan pengujian yang memakan waktu atau kebutuhan untuk konfigurasi khusus lingkungan pada server yang dibangun.
Dengan anyStub, Anda tidak perlu melumpuhkan spring-contex. Sebaliknya, menjaga konteks dekat dengan konfigurasi produksi sederhana dan mudah.
Dalam contoh ini, kita akan melihat bagaimana menghubungkan anyStub ke Layanan Web RESTful dari manual dari Pivotal.
Menghubungkan perpustakaan melalui pom.xml
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
Langkah selanjutnya adalah mengubah konteks pegas.
package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
Modifikasi ini tidak mengubah hubungan komponen dalam aplikasi, tetapi hanya menggantikan implementasi antarmuka tunggal. Ini mengirim kami ke Prinsip Substitusi Barbara Lisk . Jika desain aplikasi Anda tidak melanggarnya, maka pergantian ini tidak akan melanggar fungsionalitas.
Semuanya sudah siap. Proyek ini sudah termasuk tes.
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
Tes ini kosong, tetapi sudah menjalankan konteks aplikasi. Kesenangan dimulai di sini . Seperti yang kami katakan di atas, konteks aplikasi dalam tes bertepatan dengan konteks kerja di mana CommandLineRunner dibuat di mana permintaan http ke sistem eksternal dieksekusi.
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
Ini cukup untuk menunjukkan operasi perpustakaan. Setelah memulai tes untuk pertama kalinya, Anda akan menemukan file complete/src/test/resources/anystub/stub.yml
yang baru complete/src/test/resources/anystub/stub.yml
.
request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
Apa yang terjadi spring-boot telah membangun RestTemplateBuilder dari konfigurasi pengujian ke dalam aplikasi. Ini menyebabkan aplikasi bekerja melalui implementasi rintisan klien http. StubHttpClient mencegat permintaan, tidak menemukan file rintisan, mengeksekusi permintaan, menyimpan hasil dalam file, dan mengembalikan hasil yang dipulihkan dari file.
Mulai sekarang, Anda dapat menjalankan tes ini tanpa koneksi internet dan permintaan ini akan berhasil. restTemplate.getForObject()
akan mengembalikan hasil yang sama. Anda dapat mengandalkan fakta ini di tes mendatang.
Anda dapat menemukan semua perubahan yang dijelaskan di GitHub .
Faktanya, kami masih belum membuat satu tes. Sebelum menulis tes, mari kita lihat cara kerjanya dengan database.
Dalam contoh ini, kami akan menambahkan tes integrasi ke Mengakses Data Relasional menggunakan JDBC dengan Spring dari tutorial Pivotal.
Konfigurasi tes untuk kasus ini terlihat seperti ini:
package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
Di sini, sumber data reguler ke database eksternal dibuat dan dibungkus dengan implementasi rintisan - kelas StubDataSource. Spring-boot menyematkannya dalam konteks. Kita juga perlu membuat setidaknya satu tes untuk menjalankan konteks pegas dalam tes.
package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
Sekali lagi ini adalah tes kosong - satu-satunya tugas adalah menjalankan konteks aplikasi. Di sini kita melihat anotasi @AnystubId
sangat penting, tetapi belum terlibat.
Setelah dijalankan pertama kali, Anda akan menemukan file src/test/resources/anystub/stub.yml
yang mencakup semua panggilan basis data. Anda akan terkejut bagaimana pegas bekerja di belakang layar dengan database. Perhatikan bahwa, uji coba yang baru tidak akan menghasilkan akses nyata ke database. Jika Anda menghapus test.mv.db, itu tidak akan muncul setelah pengujian berulang kali. Set lengkap perubahan dapat dilihat di GitHub .
Untuk meringkas. dengan anyStub:
- Anda tidak perlu secara khusus mengatur lingkungan pengujian
- pengujian dilakukan dengan data nyata
- uji coba pertama membuktikan asumsi Anda dan menyimpan data uji, yang berikutnya memeriksa bahwa sistem belum terdegradasi
Anda mungkin memiliki pertanyaan: bagaimana hal ini mencakup kasus di mana basis data belum ada, apa yang harus dilakukan dengan pengujian negatif dan penanganan pengecualian. Kami akan kembali ke ini, tetapi pertama-tama, kami akan berurusan dengan menulis tes sederhana.
Kami sekarang bereksperimen dengan Mengkonsumsi Layanan Web RESTful . Proyek ini tidak mengandung komponen yang dapat diuji. Dua kelas dibuat di bawah ini, yang harus menggambarkan dua lapisan dari beberapa desain arsitektur.
package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
DataProvider menyediakan akses ke data di volatile sistem eksternal.
package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
DataProcessor akan memproses data dari sistem eksternal.
Kami bermaksud menguji DataProcessor
. Penting untuk menguji kebenaran algoritma pemrosesan dan melindungi sistem dari degradasi dari perubahan di masa depan.
Untuk mencapai tujuan ini, Anda dapat mempertimbangkan membuat objek tiruan DataProvider dengan kumpulan data dan meneruskannya ke konstruktor DataProcessor dalam pengujian. Cara lain bisa dengan menguraikan DataProcessor untuk menyoroti pemrosesan kelas Penawaran. Kemudian, kelas semacam itu mudah untuk diuji menggunakan unit test (Tentunya, ini adalah metode yang direkomendasikan dalam buku-buku yang dihormati tentang kode bersih). Mari kita coba untuk menghindari perubahan kode dan penemuan data uji dan hanya menulis tes.
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
Saatnya berbicara tentang anotasi @AnystubId. Anotasi ini membantu mengelola dan mengontrol file rintisan dalam pengujian. Itu dapat digunakan dengan kelas uji atau metodenya. Anotasi ini mengatur file rintisan individu untuk area yang sesuai. Jika ada area yang secara bersamaan dicakup oleh anotasi level kelas dan metode, anotasi metode lebih diutamakan. Anotasi ini memiliki parameter nama file, yang menentukan nama file rintisan. ekstensi ".yml" ditambahkan secara otomatis jika dihilangkan. Dengan menjalankan tes ini Anda tidak akan menemukan file baru. File src/test/resources/anystub/stub.yml
telah dibuat sebelumnya dan tes ini akan menggunakannya kembali. Kami mendapat nomor 131 dari rintisan ini dengan menganalisis hasil kueri.
@Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
Dalam pengujian ini, anotasi @AnyStubId muncul tanpa parameter nama file. Dalam kasus ini, file src/test/resources/anystubprocessDataTest2.yml
. Nama file dibangun dari nama fungsi (kelas) + ".yml". Setelah anyStub membuat file baru untuk pengujian ini, Anda perlu membuat panggilan sistem yang sebenarnya. Dan itu adalah keberuntungan kami bahwa kutipan baru memiliki panjang yang sama. Dua pemeriksaan terakhir menunjukkan cara menguji perilaku aplikasi. Ini tersedia untuk Anda: memilih kueri berdasarkan parameter atau bagian parameter dan menghitung jumlah kueri. Ada beberapa variasi waktu dan fungsi pencocokan yang dapat ditemukan dalam dokumentasi .
@Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
Dalam pengujian ini, @AnyStubId muncul dengan parameter requestMode baru. Ini memungkinkan Anda untuk mengelola izin untuk file rintisan. Ada dua aspek untuk dikendalikan: pencarian file dan izin untuk memanggil sistem eksternal.
RequestMode.rmTrack
menetapkan aturan berikut: jika file baru saja dibuat, semua permintaan dikirim ke sistem eksternal dan ditulis ke file dengan jawaban, terlepas dari apakah ada permintaan yang identik dalam file (duplikat dalam file diperbolehkan). Jika sebelum menjalankan tes, file rintisan ada, permintaan ke sistem eksternal dilarang. Panggilan diharapkan dalam urutan yang persis sama. Jika permintaan berikutnya tidak cocok dengan permintaan dalam file, pengecualian dilemparkan.
RequestMode.rmNew
mode ini diaktifkan secara default. Setiap permintaan dicari dalam file rintisan. Jika permintaan yang cocok ditemukan - hasil yang sesuai dipulihkan dari file, permintaan ke sistem eksternal ditunda. Jika permintaan tidak ditemukan, sistem eksternal diminta, hasilnya disimpan dalam file. Permintaan duplikat dalam file - jangan terjadi.
RequestMode.rmNone
Setiap permintaan dicari dalam file rintisan. Jika kueri yang cocok ditemukan, hasilnya dikembalikan dari file. Jika tes menghasilkan permintaan yang tidak ada dalam file, pengecualian dilemparkan.
RequestMode.rmAll
sebelum permintaan pertama, file rintisan dihapus. Semua permintaan ditulis ke file (duplikat dalam file diizinkan). Anda dapat menggunakan mode ini jika Anda ingin menonton koneksi berfungsi.
RequestMode.rmPassThrough
semua permintaan dikirim langsung ke sistem eksternal, melewati rintisan implementasi.
Perubahan ini tersedia di GitHub.
Apa lagi
Kami melihat bagaimana anyStub menyimpan respons. Jika pengecualian dilemparkan ketika mengakses sistem eksternal, anyStub akan menyimpannya, dan akan memutarnya pada permintaan berikutnya.
Seringkali pengecualian dilemparkan oleh kelas tingkat atas, sementara kelas koneksi menerima respons yang valid (Mungkin dengan kode kesalahan). Dalam hal ini, anyStub bertanggung jawab untuk mereproduksi jawaban dengan kode kesalahan, dan kelas tingkat yang lebih tinggi juga akan memberikan pengecualian untuk pengujian Anda.
Tambahkan file rintisan ke repositori.
Jangan takut untuk menghapus dan menimpa file rintisan.
Kelola file rintisan dengan bijak. Anda dapat menggunakan kembali satu file dalam beberapa tes atau memberikan file individual untuk setiap tes. Gunakan kesempatan ini untuk kebutuhan Anda. Tetapi biasanya menggunakan satu file dengan mode akses berbeda adalah ide yang buruk.
Ini semua adalah fitur utama anyStub.