Aplikasi TDD pada Spring Boot: menyelaraskan tes dan bekerja dengan konteksnya

Artikel ketiga dalam seri dan cabang kecil dari seri utama - kali ini saya akan menunjukkan cara kerja Spring Integration Testing Library dan cara kerjanya, apa yang terjadi ketika tes dimulai dan bagaimana Anda dapat menyempurnakan aplikasi dan lingkungannya untuk pengujian.


Saya diminta untuk menulis artikel ini dengan komentar Hixon10 tentang cara menggunakan basis nyata, seperti Postgres, dalam tes integrasi. Penulis komentar menyarankan untuk menggunakan pustaka yang disematkan semua pangkalan data yang disertakan. Dan saya sudah menambahkan paragraf dan contoh penggunaan dalam kode, tapi kemudian saya memikirkannya. Tentu saja, mengambil perpustakaan yang sudah jadi adalah benar dan baik, tetapi jika tujuannya adalah untuk memahami cara menulis tes untuk aplikasi Musim Semi, maka akan lebih berguna untuk menunjukkan bagaimana menerapkan sendiri fungsi yang sama. Pertama, ini adalah alasan yang bagus untuk membicarakan apa yang ada di balik tudung Uji Musim Semi . Dan kedua, saya percaya bahwa Anda tidak bisa bergantung pada perpustakaan pihak ketiga, jika Anda tidak mengerti bagaimana mereka diatur di dalam, ini hanya mengarah pada penguatan mitos "keajaiban" teknologi.


Kali ini tidak akan ada fitur pengguna, tetapi akan ada masalah yang perlu diselesaikan - Saya ingin memulai database nyata pada port acak dan menghubungkan aplikasi ke database sementara ini secara otomatis, dan setelah tes saya berhenti dan menghapus database.


Pada awalnya, seperti yang sudah biasa, sedikit teori. Untuk orang-orang yang tidak terlalu terbiasa dengan konsep bin, konteks, konfigurasi, saya merekomendasikan pengetahuan yang menyegarkan, misalnya, dalam artikel saya Sisi sebaliknya dari Spring / Habr .


Tes pegas


Uji Musim Semi adalah salah satu perpustakaan yang termasuk dalam Kerangka Kerja Musim Semi, pada kenyataannya, segala sesuatu yang dijelaskan dalam bagian dokumentasi tentang pengujian integrasi hanyalah tentang itu. Empat tugas utama yang dipecahkan perpustakaan adalah:


  • Kelola wadah IoC Musim Semi dan penyimpanannya di antara pengujian
  • Berikan injeksi ketergantungan untuk kelas tes
  • Berikan manajemen transaksi yang cocok untuk tes integrasi
  • Berikan satu set kelas dasar untuk membantu pengembang menulis tes integrasi

Saya sangat merekomendasikan membaca dokumentasi resmi, katanya banyak hal yang berguna dan menarik. Di sini saya akan memberikan pemerasan cepat dan beberapa tips praktis yang berguna untuk diingat.


Uji siklus hidup



Siklus hidup tes terlihat seperti ini:


  1. Ekstensi untuk kerangka uji ( SpringRunner untuk JUnit 4 dan SpringExtension untuk JUnit 5) memanggil Test Context Bootstrapper
  2. Boostrapper menciptakan TestContext - kelas utama yang menyimpan status pengujian dan aplikasi saat ini
  3. TestContext mengatur kait yang berbeda (seperti memulai transaksi sebelum pengujian dan rollback setelah), menyuntikkan dependensi ke dalam kelas uji (semua bidang @Autowired pada kelas tes) dan membuat konteks
  4. Konteks dibuat menggunakan Context Loader - ia mengambil konfigurasi dasar aplikasi dan menggabungkannya dengan konfigurasi pengujian (properti, profil, nampan, penginstalasi, dan lain-lain yang tumpang tindih).
  5. Konteks di-cache menggunakan kunci komposit yang sepenuhnya menggambarkan aplikasi - satu set tempat sampah, properti, dll.
  6. Tes berjalan

Semua pekerjaan kotor dalam mengelola tes dilakukan, pada kenyataannya, dengan spring-test , dan Spring Boot Test pada gilirannya, menambahkan beberapa kelas pembantu, seperti @DataJpaTest dan @SpringBootTest , utilitas bermanfaat seperti TestPropertyValues untuk mengubah properti konteks secara dinamis. Ini juga memungkinkan Anda untuk menjalankan aplikasi sebagai server web nyata, atau sebagai lingkungan tiruan (tanpa akses melalui HTTP), akan lebih mudah untuk menghapus komponen sistem menggunakan @MockBean , dll.

Caching Konteks


Mungkin salah satu topik yang sangat tidak jelas dalam pengujian integrasi yang menimbulkan banyak pertanyaan dan kesalahpahaman adalah caching konteks (lihat paragraf 5 di atas) antara tes dan pengaruhnya terhadap kecepatan tes. Komentar yang sering saya dengar adalah bahwa tes integrasi "lambat" dan "jalankan aplikasi untuk setiap tes." Jadi, mereka menjalankan - tetapi tidak untuk setiap tes. Setiap konteks (mis. Instance aplikasi) akan digunakan kembali secara maksimal, mis. jika 10 tes menggunakan konfigurasi aplikasi yang sama, maka aplikasi akan mulai satu kali untuk semua 10 tes. Apa arti "konfigurasi yang sama" dari aplikasi? Untuk Uji Musim Semi, ini berarti set kacang, kelas konfigurasi, profil, properti, dll., Tidak berubah. Dalam praktiknya, ini berarti bahwa, misalnya, dua tes ini akan menggunakan konteks yang sama:


 @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { } 

Jumlah konteks dalam cache dibatasi hingga 32 - lebih lanjut, sesuai dengan prinsip LRSU, salah satunya akan dihapus dari cache.

Apa yang dapat mencegah Uji Coba menggunakan kembali konteks dari cache dan membuat yang baru?


@DirtiesContext
Opsi termudah adalah jika tes ditandai dengan anotasi, konteksnya tidak akan di-cache. Ini dapat berguna jika tes mengubah keadaan aplikasi dan Anda ingin "mengatur ulang" itu.


@MockBean
Opsi yang sangat tidak jelas, saya bahkan merendernya secara terpisah - @MockBean menggantikan kacang asli dalam konteks dengan tiruan yang dapat diuji melalui Mockito (dalam artikel berikut saya akan menunjukkan cara menggunakannya). Poin kuncinya adalah bahwa anotasi ini mengubah set kacang dalam aplikasi dan memaksa Uji Musim Semi untuk menciptakan konteks baru. Jika kita mengambil contoh sebelumnya, misalnya, dua konteks sudah akan dibuat di sini:


 @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { @MockBean CakeFinder cakeFinderMock; } 

@TestPropertySource
Setiap perubahan properti secara otomatis mengubah kunci cache dan konteks baru dibuat.


@ActiveProfiles
Mengubah profil yang aktif juga akan memengaruhi cache.


@ContextConfiguration
Dan tentu saja, perubahan konfigurasi apa pun juga akan membuat konteks baru.


Kami memulai pangkalan


Jadi sekarang dengan semua pengetahuan ini kami akan mencoba lepas landas memahami bagaimana dan di mana Anda dapat menjalankan basis data. Tidak ada jawaban yang benar di sini, itu tergantung pada persyaratan, tetapi Anda dapat memikirkan dua opsi:


  1. Jalankan sekali sebelum semua tes di kelas.
  2. Jalankan instance acak dan database terpisah untuk setiap konteks yang di-cache (berpotensi lebih dari satu kelas).

Tergantung pada persyaratan, Anda dapat memilih opsi apa pun. Jika dalam kasus saya, Postgres mulai relatif cepat dan opsi kedua terlihat cocok, maka yang pertama mungkin cocok untuk sesuatu yang lebih sulit.


Opsi pertama tidak terikat pada Spring, melainkan pada kerangka uji. Misalnya, Anda dapat membuat Ekstensi untuk JUnit 5 Anda .

Jika Anda mengumpulkan semua pengetahuan tentang pustaka tes, konteks dan caching, tugas bermuara sebagai berikut: saat membuat konteks aplikasi baru, Anda perlu menjalankan database pada port acak dan mentransfer data koneksi ke konteks .


Antarmuka ApplicationContextInitializer bertanggung jawab untuk melakukan tindakan dengan konteks sebelum diluncurkan di Spring.


ApplicationContextInitializer


Antarmuka hanya memiliki satu metode initialize , yang dieksekusi sebelum konteksnya "dimulai" (yaitu, sebelum metode refresh dipanggil) dan memungkinkan Anda untuk membuat perubahan pada konteks - tambahkan nampan, properti.


Dalam kasus saya, kelasnya terlihat seperti ini:


 public class EmbeddedPostgresInitializer implements ApplicationContextInitializer<GenericApplicationContext> { @Override public void initialize(GenericApplicationContext applicationContext) { EmbeddedPostgres postgres = new EmbeddedPostgres(); try { String url = postgres.start(); TestPropertyValues values = TestPropertyValues.of( "spring.test.database.replace=none", "spring.datasource.url=" + url, "spring.datasource.driver-class-name=org.postgresql.Driver", "spring.jpa.hibernate.ddl-auto=create"); values.applyTo(applicationContext); applicationContext.registerBean(EmbeddedPostgres.class, () -> postgres, beanDefinition -> beanDefinition.setDestroyMethodName("stop")); } catch (IOException e) { throw new RuntimeException(e); } } } 

Hal pertama yang terjadi di sini adalah embedded Postgres diluncurkan dari pustaka yandex-qatools / postgresql-embedded . Kemudian, serangkaian properti dibuat - URL JDBC untuk basis yang baru diluncurkan, tipe driver, dan perilaku Hibernate untuk skema (secara otomatis dibuat). Satu hal yang tidak jelas adalah hanya spring.test.database.replace=none - ini adalah apa yang kami katakan kepada DataJpaTest bahwa kami tidak harus mencoba untuk terhubung ke database tertanam, seperti H2, dan kami tidak perlu mengganti nampan DataSource (ini berfungsi).


Dan poin penting lainnya adalah application.registerBean(…) . Secara umum, kacang ini tentu saja tidak dapat didaftarkan - jika tidak ada yang menggunakannya dalam aplikasi, maka kacang ini tidak terlalu dibutuhkan. Registrasi hanya diperlukan untuk menentukan metode penghancuran yang akan dipanggil Spring ketika konteksnya dihancurkan, dan dalam kasus saya, metode ini akan memanggil postgres.stop() dan menghentikan basis data.


Secara umum, itu saja, keajaiban berakhir, jika ada. Sekarang saya akan mendaftarkan inisialisasi ini dalam konteks pengujian:


 @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) ... 

Atau bahkan untuk kenyamanan, Anda dapat membuat anotasi Anda sendiri, karena kita semua menyukai anotasi!


 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) public @interface EmbeddedPostgresTest { } 

Sekarang setiap tes yang dianotasikan oleh @EmbeddedPostgrestTest akan memulai database pada port acak dan dengan nama acak, konfigurasikan Spring untuk terhubung ke database ini dan menghentikannya di akhir tes.


 @EmbeddedPostgresTest class JpaCakeFinderTestWithEmbeddedPostgres { ... } 

Kesimpulan


Saya ingin menunjukkan bahwa tidak ada sihir misterius di Spring, hanya ada banyak mekanisme internal yang "pintar" dan fleksibel, tetapi mengetahui mereka Anda bisa mendapatkan kontrol penuh pada tes dan aplikasi itu sendiri. Secara umum, dalam proyek pertempuran, saya tidak memotivasi semua orang untuk menulis metode dan kelas mereka sendiri untuk mengatur lingkungan integrasi untuk tes, jika ada solusi yang siap pakai, maka Anda dapat mengambilnya. Meskipun jika keseluruhan metode adalah 5 baris kode, maka mungkin menyeret ketergantungan ke dalam proyek, terutama tidak memahami implementasi, adalah berlebihan.


Tautan ke artikel lain dalam seri ini


Source: https://habr.com/ru/post/id446184/


All Articles