Pengujian integrasi layanan microser di Scala

Pengujian unit memang bagus, tetapi tidak cukup. Seringkali Anda ingin memastikan bahwa aplikasi yang berjalan juga berfungsi. Tes integrasi datang untuk menyelamatkan. Ini semakin banyak digunakan untuk menguji layanan, dan Docker memungkinkan Anda untuk mengelola lingkungan pengujian Anda dengan mudah. Tetapi, seperti biasa, segala sesuatunya tidak sesederhana itu ketika ada lebih banyak layanan dan ketergantungan microser.

Yuri Badalyants di RIT ++ memberi tahu bagaimana di 2GIS mereka menguji banyak layanan dan kebun binatang teknologi secara keseluruhan. Di bawah potongan, versi laporan ini ditambahkan dan diperbarui di bawah pengawasan yang cermat dari pembicara: opsi apa yang telah Anda coba, apa yang Anda lakukan, masalah apa yang tidak harus Anda selesaikan sekarang. Ini akan tentang Docker, Testcontainers, dan juga tentang Scala.


Tentang pembicara: Yuri Badalyants (@ LMnet ) memulai karirnya pada tahun 2011 sebagai pengembang web, bekerja dengan PHP, JavaScript dan Java. Sekarang dia menulis di Scala di 2GIS.

Kasino


2GIS telah menyediakan peta kota dan direktori perusahaan yang nyaman selama 20 tahun, dan baru-baru ini kami memiliki versi baru dengan peta Rusia tanpa batas. Saya akan memberi tahu Anda tentang pengalaman yang didapat saat saya bekerja di tim Kasino. Tim ini terlibat dalam tiga bidang utama:

  • Iklan - pengiklan mana yang akan ditampilkan, yang disembunyikan, yang dinaikkan dan cara menurunkan peringkat.

  • BigData terkait dengan periklanan dan personalisasi, serta pembuatan analisis dan metrik.
  • Crawler adalah program yang mencari organisasi di Internet untuk secara otomatis menambahkannya ke database.

Ketiga area ini adalah tugas utama, yang, pada gilirannya, memiliki sejumlah besar subtugas. Saat ini, ada lebih dari 25 microservices yang ditulis dalam Scala. Ini hanya kode kami, tetapi kami juga menggunakan sistem pihak ketiga, seperti PostgreSQL, Cassandra dan Kafka. Kami menyimpan data di Hadoop dan memprosesnya di Spark. Selain itu, kami menggunakan metode pembelajaran mesin yang disediakan oleh tim Ilmu Data.

Akibatnya, kami memiliki sejumlah besar layanan dan layanan mikro, sejumlah besar dependensi, dan, tentu saja, semua ini perlu diuji dalam beberapa cara.

Tentu saja, kami menulis unit test. Namun, bahkan jika semua tes berwarna hijau, ini tidak berarti semuanya bekerja. Sesuatu mungkin salah selama fase integrasi komponen atau layanan Microsoft. Oleh karena itu, kami menulis tes integrasi.

Tes integrasi


Setiap layanan mikro yang dikembangkan oleh tim Kasino menyelesaikan masalah bisnisnya dan terletak di repositori terpisah di GitLab. Artikel ini akan fokus pada pengujian integrasi dalam satu repositori (microservice) dengan dependensi yang dikunci, yang merupakan tanggung jawab pengembang sendiri. Tim QA sedang menguji interaksi layanan mikro, dan saya tidak akan menyentuh topik ini.

Ketika saya pertama kali bergabung dengan tim, pada akhir 2016, ada sekitar skema tes integrasi berikut:


  1. Pengembang mendorong kode-nya di GIT, setelah itu kode microservice masuk ke TeamCity. TeamCity mulai membuat kode dan menjalankan tes.
  2. TeamCity mengambil file konfigurasi (config) dari Chef (sistem manajemen konfigurasi yang mirip dengan Ansible, hanya ditulis dalam Ruby). Chef juga berfungsi untuk mengotomatiskan penyebaran. Ketika saya memiliki 100 mesin, saya tidak ingin pergi ke masing-masing dan menginstal apa yang saya butuhkan di SSH, dan Chef memungkinkan saya untuk mengotomatisasi ini.
  3. TeamCity mengumpulkan file jar (karena kami menulis di Scala, artefak yang kami terbitkan adalah jar), maka program memuatnya ke lingkungan CI. Aplikasi kami ditempatkan di sana, ada juga beberapa dependensi. Dalam diagram, salah satu dependensi digambarkan sebagai basis data. Mungkin ada banyak ketergantungan seperti itu, dan terima kasih kepada Chef, aplikasi kita tahu tentang mereka dan mulai berinteraksi dengan mereka.
  4. Selanjutnya, TeamCity meluncurkan SBT (ini adalah sistem build kami, tempat kompilasi dan tes dijalankan) dan menjalankan tes sendiri. Mereka relatif mirip dengan tes unit, tetapi mereka bekerja terutama pada prinsip ini: pergi melalui http ke alamat tertentu, periksa beberapa metode dan lihat apa yang dikembalikan; atau melakukan persiapan, dan kemudian melihat apakah yang dibutuhkan telah kembali.

Apa yang bisa dikatakan tentang skema seperti itu? Yang terpenting, ini berhasil. Ketika semuanya sudah diatur, menjalankan tes mudah, karena terlihat seperti tes unit. Namun plus berakhir di sana.

Dan kontra mulai. Lingkungan CI selalu hidup , dan ini merupakan pemborosan sumber daya tambahan. Karena Chef adalah konfigurasi statis, Anda harus selalu memiliki beberapa jenis mesin di mana semua dependensi akan dikonfigurasikan, di mana aplikasi akan digunakan secara independen. Mesin seperti itu akan menghabiskan sumber daya tambahan, karena pengujian dijalankan dari waktu ke waktu, dan mesin harus siap setiap saat. Selain itu, lingkungan CI disertakan dengan semua dependensi.

Tidak mungkin menjalankan tes pada dua cabang secara bersamaan . Ini mengikuti dari paragraf sebelumnya: karena kita memiliki satu lingkungan, kita tidak bisa menjalankannya secara paralel.

Tes, start, stop, dan restart tidak mungkin dilakukan . Saya akan menjelaskan mengapa ini perlu: semua aplikasi kita mematuhi logika yang disebut anggun shutdown , yaitu, ketika kita mendapatkan SIGTERM, kita tidak menghentikan proses di tengah, tetapi mencegat sinyal ini dan memahami bahwa kita perlu mematikan program. Pada titik ini, logika tertentu dihidupkan, misalnya, permintaan HTTP yang sedang "dalam penerbangan" diproses, atau jika kami bekerja dengan Kafka, kami melakukan semua kesalahan - dengan kata lain, kami melakukan tindakan tertentu sehingga kami dapat menyelesaikan pekerjaan dengan aman, dan lalu, ketika semuanya dilakukan, matikan.

Logika ini tidak selalu sederhana, dan Anda dapat mengujinya dengan skema seperti itu hanya secara manual, karena dari tes kami tidak mengontrol siklus hidup aplikasi. Ternyata TeamCity entah bagaimana telah menyebarkan sesuatu melalui Chef, sementara pengujiannya berada pada tahap yang berbeda dan tidak tahu bagaimana aplikasi tersebut digunakan.

Minus berikutnya adalah sangat sulit untuk mengkonfigurasi semua ini secara lokal . Artinya, ada banyak dependensi, mereka memiliki konfigurasi sendiri, mereka perlu dinaikkan pada mesin lokal. Aplikasi itu sendiri juga memiliki file konfigurasi sendiri, di mana ada banyak nilai. Tes itu sendiri memiliki konfigurasi yang perlu dicocokkan dengan konfigurasi aplikasi, dan mungkin juga ada lebih dari satu nilai konfigurasi. Tampaknya semua ini tidak terdengar menakutkan, seperti "pergi dan perbaiki konfigurasi di tiga tempat", tetapi dalam kenyataannya mungkin diperlukan berjam-jam bagi karyawan baru untuk melakukan ini.

GitLab CI + Docker


Seiring waktu, skema ini telah berubah menjadi yang lain: GitLab CI dan Docker . Ini tidak terjadi karena skema sebelumnya tidak ideal, tetapi karena perusahaan sedikit mengubah arah dalam hal administrasi organisasi.

Sebelumnya, masing-masing tim, dan kami memiliki banyak dari mereka, seperti yang kami inginkan atau bagaimana kami bisa, dan mengerahkan pekerjaannya. Misalnya, kami memiliki TeamCity, Chef, dan tim lain yang bisa menggunakan Jenkins atau Ansible.

Sekarang kami bergerak menuju cloud lokal dan Kubernetes, dan ada tim terpisah yang mengelola semua ini, baik GitLab CI dan Kubernetes. Tim lain hanya menggunakan ini sebagai layanan. Ini jauh lebih nyaman karena Anda tidak perlu mengelola semua ini secara manual.

Menggunakan Kubernetes, kami menerapkan skema berikut:


  1. Alih-alih TeamCity, Gitlab CI sekarang digunakan.
  2. GitLab CI membuat gambar buruh pelabuhan dan menyebarkannya ke Kubernetes. Konfigurasi sekarang disimpan langsung di repositori, dan tidak secara terpisah di Chef, jadi untuk penyebaran Anda tidak perlu bekerja dengan layanan konfigurasi pihak ketiga.
  3. Ketergantungan meningkat di muka, juga di Kubernetes.
  4. Kemudian GitLab CI meluncurkan SBT dan menguji dalam langkah terpisah.

Semuanya sangat mirip dengan skema sebelumnya dan pada dasarnya tidak berbeda dari itu, yaitu, bahkan pro dan kontra akan persis sama, tetapi Docker muncul.

Dengan buruh pelabuhan, Anda dapat melakukan lebih banyak hal menyenangkan yang berbeda dan salah satunya adalah pembuatan docker.

Susunan docker


Ini adalah semacam "overlay" pada Docker, yang memungkinkan Anda untuk menjalankan beberapa gambar docker sebagai satu kesatuan.

Contoh yang bagus di mana komposisi buruh pelabuhan sangat membantu adalah Kafka. Dia membutuhkan ZooKeeper untuk berlari. Jika Anda mengangkat Kafka dan ZooKeeper tanpa membuat docker, maka Anda perlu menaikkan ZooKeeper secara terpisah di docker, secara terpisah - Kafka, dan menjaga agar kedua wadah buruh pelabuhan ini konsisten. Ini sangat tidak nyaman, dan menulis buruh pelabuhan memungkinkan Anda untuk mendeskripsikan kedua wadah dalam satu file buruh pelabuhan-menulis.yml dan menggunakan perintah docker-compose run Kafka sederhana docker-compose run Kafka meningkatkan Kafka dan ZooKeeper.

Anda dapat membuat tes integrasi pada komposisi buruh pelabuhan. Mari kita lihat bagaimana tampilannya.


  1. Sekali lagi, dorong semuanya di GitLab.
  2. GitLab CI meluncurkan docker-compose.
  3. Dalam menulis docker, aplikasi naik, semua dependensi dan SBT naik, dan SBT mendorong tes untuk aplikasi ini - semuanya terjadi di dalam penulisan docker.

Berkat skema ini, tidak perlu lagi menjaga lingkungan dan dependensi yang terpisah, karena semuanya berjalan langsung ke runner GitLab CI, di mana buruh pelabuhan dan pembuat komposisi harus berada. Selama awal, dia akan memompa gambar yang diperlukan dan menjalankannya.

Selain itu, Anda dapat menguji cabang yang berbeda secara bersamaan, karena semuanya terjadi pada pelari.

Sekarang lebih mudah untuk mengkonfigurasi lingkungan secara lokal , tetapi Anda masih perlu mengoordinasikan beberapa tempat. Intinya adalah bahwa sekarang, ketika kita melakukan konfigurasi lokal, kita tidak perlu meletakkan semuanya pada mesin lokal, semuanya ditulis dalam file docker-compose.yml. Jadi, Anda harus mengonfigurasi di dua tempat yang berbeda - ini adalah docker-compose.yml dan konfigurasi pengujian kami.

Sedangkan untuk minusnya, masih tidak mungkin untuk menguji mulai, berhenti dan mulai ulang , karena dari SBT, dari tes, kami tidak mengontrol siklus hidup aplikasi. Itu dijalankan oleh buruh pelabuhan-menulis, itu menjalankan SBT dan tes dijalankan di dalam SBT. Dengan demikian, tidak ada manajemen siklus hidup aplikasi yang lengkap. Ada juga kesulitan dengan peluncuran, yang ingin saya bicarakan lebih banyak.

docker-compose 2


Pada zaman docker-compose 2, docker-compose.yml file tersebut terlihat seperti ini:

 version: '2.1' services: web: build: . depends_on: db: condition: service_healthy redis: condition: service_started redis: image: redis db: image: db healthcheck: test: "some test here" 

Layanan terdaftar di sini, yaitu, apa yang akan kami tingkatkan sebagai bagian dari komposisi buruh pelabuhan ini. Dalam hal ini, saya baru saja mengambil contoh dari dokumentasi docker-compose. Ada tiga layanan: web, redis, dan db (database).

Web adalah aplikasi kita, dan redis dan db adalah semacam dependensi.

Ada item di blok web yang disebut depends_on . Ini menunjukkan bahwa aplikasi web tergantung pada beberapa wadah lain, dan dijelaskan di bawah ini: dari database dan redis.

Juga, ada klausa condition . Untuk redis, ini adalah service_started , yang berarti bahwa sampai redis dimulai, kontainer tidak akan mencoba untuk memulai aplikasi web.

Sedangkan untuk database, kondisinya adalah service_healthy , dan pemeriksaan kesehatan dijelaskan di bawah ini. Artinya, kita tidak hanya perlu meluncurkan wadah buruh pelabuhan, tetapi juga untuk melakukan pemeriksaan kesehatan tertentu. Ini bisa berupa logika khusus.

Sebagai contoh, kami menggunakan PostgreSQL, yang menggunakan ekstensi PostGIS, dan perlu beberapa saat untuk menginisialisasi. Ketika kami meluncurkan wadah buruh pelabuhan, kami tidak dapat segera bekerja dengan ekstensi postgis - kami harus menunggu ekstensi untuk menginisialisasi. Karenanya, kami hanya SELECT PostGIS_Version(); kueri SELECT PostGIS_Version(); ke SELECT PostGIS_Version(); . Sampai ekstensi diinisialisasi, permintaan akan melempar kesalahan, dan ketika ekstensi diinisialisasi, itu akan mulai mengembalikan versi. Ini sangat mudah dan logis - pertama kita akan meningkatkan semua dependensi, dan kemudian aplikasi .

docker-compose 3


Ketika docker-compose 3 keluar, kami mulai menggunakannya.

Tetapi dalam dokumentasi untuk itu, item muncul pada perubahan logika depend_on. Pengembang buruh pelabuhan memutuskan bahwa deskripsi grafik ketergantungan sudah cukup. Ini berarti bahwa ketika Anda memulai docker-compose run web , baik aplikasi itu sendiri dan db di mana itu tergantung akan mulai secara bersamaan.



Paragraf berikutnya dalam dokumentasi mengatakan bahwa depend_on tidak lagi merupakan kondisi.

Jadi, jika Anda masih ingin mendapatkan fungsionalitas yang digunakan dalam versi kedua, Anda harus mengambil semuanya ke tangan Anda sendiri.

Halaman Mengontrol startup order menawarkan beberapa solusi. Opsi pertama adalah menggunakan wait-for-it.sh .

Sekarang docker-compose.yml terlihat sedikit berbeda:

 version: '3' services: web: build: . depends_on: [ db, redis ] redis: image: redis command: [ "./wait-for-it.sh", ... ] db: image: redis command: [ "./wait-for-db.sh", ... ] 

depends_on hanyalah sebuah array, tidak ada ketentuan.

Dalam dependensi kami, kami mendefinisikan kembali perintah, yaitu, dalam penulisan buruh pelabuhan Anda dapat melampirkan perintah yang dimulai dengan wadah buruh pelabuhan.

Di sana kita harus menulis wait-for-it.sh, dan yang lainnya. Alih-alih tiga poin dalam contoh di atas, kita harus menulis apa yang perlu kita tunggu, serta perintah asli yang meluncurkan wadah buruh pelabuhan.

Untuk melakukan ini, Anda perlu menemukan file buruh pelabuhan, salin perintah untuk redis dari sana dan tempel, hal yang sama berlaku untuk database. Sebuah minus yang sangat besar adalah bahwa abstraksi rusak - saya tidak ingin tahu perintah mana yang meluncurkan wadah buruh pelabuhan. Perintah-perintah ini bisa nontrivial, cukup rumit, tetapi saya tidak ingin repot, saya hanya ingin memasukkan docker run dan hanya itu.

Saya pribadi tidak terlalu menyukai solusi ini, tetapi kami memiliki beberapa layanan yang berfungsi seperti ini.

Script di atas komposisi buruh pelabuhan


Kemudian saya memutuskan bahwa saatnya telah tiba untuk " membangun sepeda", dan saya memiliki buruh pelabuhan-compose-run.sh :

 version: '3' services: postgres: ... my_service: depends_on: [ postgres ] ... sbt: depends_on: [ my_service ] ... 

Biarkan saya memberi Anda contoh semi-realistis: ada postgres di docker-compose.yml, ada aplikasi my_service, yang tergantung pada postgres, dan SBT, di mana tes dijalankan dan yang tergantung pada layanan saya.

Saya menjalankan program tidak melalui docker run , tetapi melalui script docker-compose-run.sh.

Pertama, ia memulai ketergantungan yang paling dalam terlebih dahulu, dalam kasus saya ini adalah postgres. Script memulai dependensi dalam mode "daemon", artinya, ia tidak memblokir terminal:

 docker-compose up -d postgres 

Lalu saya menunggu kondisi dipenuhi oleh fungsi wait_until. Ini hampir sama dengan wait-for-it.sh, hanya untuk berbicara, dalam gaya imperatif. Ketika PostGIS mulai, terminal diblokir, yaitu, program juga menunggu, dan jika tidak menunggu, kesalahan dilemparkan dan tes berhenti bekerja.

 wait_until 10 2 docker-compose exec -T postgres psql 

Ketika PostGIS diinisialisasi, lanjutkan ke langkah berikutnya dan lakukan hal yang sama dengan layanan. Baginya, tesnya sedikit lebih sederhana: port 80 harus diikat.

 docker-compose up -d my_service wait_until 10 2 docker-compose exec -T \ my_service sh -c "netstat -ntlp | grep 80 || exit 1" 

Langkah terakhir adalah menjalankan SBT melalui perintah run, di mana tes dijalankan.

 docker-compose run sbt down $? 

Dengan demikian, semuanya dinaikkan dalam urutan yang benar, tetapi secara manual.

Pada akhirnya, fungsi down disebut, yang menerima hasil dari perintah sebelumnya. Jika "0", maka tes telah lulus, dan kami matikan saja docker-compose; jika tidak, pertama-tama kita "meludahkan" log untuk mencari tahu apa yang salah, dan baru kemudian mematikan komposisi buruh pelabuhan.

 function down { echo "Exiting with code $1" if [[ $1 -eq 0 ]]; then docker-compose down exit $1 else docker-compose logs -t postgres my_service docker-compose down exit $1 fi } 

Skema seperti itu berfungsi, tetapi tidak skalanya dengan baik. Setiap layanan harus menggambarkan docker-compose-run.sh dengan logikanya sendiri. Plus, konfigurasi peluncuran meluas antara docker-compose-run.sh dan docker-compose.yml. Nah, secara umum, sepertinya kita tidak menggunakan docker-compose, tetapi berjuang dengan kekurangannya.

Menjalankan buruh pelabuhan dari kode


Ketika skema sebelumnya dibuat, saya berpikir: jika saya sudah memiliki semuanya di buruh pelabuhan, lalu mengapa tidak menjalankannya dari kode. Saya mulai mencari solusi dan menemukan beberapa opsi.

Opsi pertama adalah cukup menggunakan klien buruh pelabuhan . Ada dua klien buruh pelabuhan utama di dunia JVM: buruh pelabuhan-java dan klien buruh pelabuhan .

Klien buruh pelabuhan memungkinkan Anda untuk menjalankan perintah buruh pelabuhan langsung dari kode menggunakan API. Artinya, alih-alih merangkai string untuk membangun perintah seperti `docker run ...` , Anda cukup membentuk perintah seperti itu dalam kode dan menjalankannya. Itu jauh lebih nyaman.

Metode ini bekerja dengan baik, dan, tentu saja, mereka dapat melakukan segalanya, namun, ini adalah level yang sangat rendah. Saya harus membuat analog docker-compose saya sendiri, yang merupakan tugas yang sangat besar.

Opsi berikutnya adalah pustaka docker-it-scala , yang membungkus kedua klien ini dan memungkinkan Anda untuk memilih backend yang akan digunakan. Dia dapat menjalankan wadah yang Anda butuhkan.

Tetapi kekurangan dari perpustakaan ini adalah bahwa ia tidak memiliki API yang sangat fleksibel dan tidak ada kontrol siklus hidup.

Saya juga tidak menyukai opsi ini, saya terus mencari dan menemukan Testcontainers . Saya ingin memberi tahu Anda lebih banyak tentang ini.

Wadah uji


Ini adalah semacam perpustakaan java untuk meluncurkan dan menguji kontainer buruh pelabuhan. Ada fasad Scala, testcontainers-scala. Di luar kotak, ada sejumlah layanan populer, misalnya, PostgreSQL, MySQL, Nginx, Kafka, Selenium. Anda dapat menjalankan wadah lain. Perpustakaan memiliki API yang cukup sederhana dan fleksibel, yang akan saya bahas lebih detail.

Wadah yang telah ditentukan


Jadi, bagaimana cara bekerja dengan wadah standar, yang ada di perpustakaan: sebenarnya, semuanya cukup sederhana, karena wadah direpresentasikan sebagai objek:

 val pgContainer: PostgreSQLContainer = PostgreSQLContainer("postgres:9.6") pgContainer.start() val pgUrl: String = pgContainer.jdbcUrl val pgPort: Int = pgContainer.mappedPort(5432) pgContainer.stop() 

Dalam hal ini, kita membuat PostgreSQLContainer , kita dapat memulainya dan mulai bekerja dengannya. Selanjutnya, kita mendapatkan jbdcUrl , yang dengannya Anda dapat terhubung ke PostgreSQL. Setelah itu kita mappedPort .

Ini berarti PostgreSQL menonjol dari port docker 5432, dan Testcontainers melihat port ini dan secara otomatis menetapkan ke beberapa port acak. Yaitu, dari tes yang kita lihat, misalnya, 32422. Tugas terjadi secara otomatis.

Wadah kustom


Tampilan berikut, yang disebut wadah kustom, juga cukup sederhana:

 class GenericContainer( imageName: String, exposedPorts: Seq[Int] = Seq(), env: Map[String, String] = Map(), command: Seq[String] = Seq(), classpathResourceMapping: Seq[(String, String, BindMode)] = Seq(), waitStrategy: Option[WaitStrategy] = None ) ... 

Ada GenericContainer tempat Anda perlu mewarisi dan menimpa sejumlah bidang. Pastikan hanya mengatur imageName - ini adalah nama wadah yang ingin kita buat.

Anda dapat mengatur exposedPorts : port yang akan ditampung wadah. Di env, Anda dapat mengatur variabel lingkungan, Anda juga dapat mengatur command untuk dijalankan.

classpathResourceMapping memungkinkan Anda untuk membuang sumber daya dari classpath ke dalam wadah buruh pelabuhan. Ini sangat mudah, misalnya, jika konfigurasi aplikasi langsung pada sumber daya pengujian. Anda cukup memetakan di dalam, dan aplikasi di dalam buruh pelabuhan mendapat akses ke konfigurasi ini.

waitStrategy adalah hal yang sangat nyaman yang hilang di docker-compose 3, sebenarnya itu adalah HealthCheck. Ada beberapa waitStrategy telah ditentukan, misalnya, Anda dapat menunggu sampai terjadi pengikatan port, atau metode http spesifik akan mengembalikan 200. Tetapi Anda dapat menulis HealthCheck Anda.

Karena Anda menulis HealthCheck hanya dalam kode Anda, Anda dapat menggunakan, pertama, bahasa normal, bukan bash, dan, kedua, setiap perpustakaan yang tersedia dari kode Anda: jika Anda ingin membuat HealthCheck kustom di Cassandra - ambil driver dan tulis Periksa Kesehatan.

Menjalankan tes


Dan sekarang sedikit tentang cara menjalankan tes:

 class PostgresqlSpec extends FlatSpec with ForAllTestContainer { override val container = PostgreSQLContainer() "PostgreSQL container" should "be started" in { Class.forName(container.driverClassName) val connection = DriverManager .getConnection(container.jdbcUrl, container.username, container.password) // test some stuff } } 

Saya akan berbicara tentang ScalaTest , standar de facto untuk pengujian di dunia Scala.

Sebagai contoh, kami ingin menulis tes untuk Postgres. Buat tes PostgresqlSpec dan mewarisinya dari ForAllTestContainer . Ini adalah sifat yang disediakan oleh perpustakaan. Ini akan memulai wadah yang diperlukan sebelum semua tes dan menghentikannya setelah semua tes. Atau Anda dapat menggunakan ForeachTestContainer , lalu wadah mulai sebelum setiap tes dan berhenti setelah masing-masing.

Maka Anda perlu mendefinisikan kembali wadah. Ini dapat dilakukan dengan mengganti properti container . Dalam kasus saya, saya menggunakan PostgreSQLContainer .

Lalu kami menulis tes. Dalam contoh ini, saya membuat koneksi, mengambil jdbcUrl, nama pengguna, kata sandi, menulis tes khusus, mengirim permintaan.

Biasanya, tes integrasi memerlukan beberapa wadah. Saya bisa membuatnya menggunakan MultipleContainers :

 val pgContainer = PostgreSQLContainer() val myContainer = MyContainer() override val container = MultipleContainers(pgContainer, myContainer) 

Yaitu, saya membuat wadah, menambahkannya ke MultipleContainers , dan menggunakannya sebagai container .

Skema untuk menjalankan tes dengan Testcontainers adalah sebagai berikut:



  1. Dorong kode di GitLa.
  2. Pelari GitLab CI meluncurkan SBT.
  3. SBT menjalankan tes. Di dalam pengujian, aplikasi dan dependensi kami diluncurkan.

Keuntungan dari skema ini:

  • Tidak perlu menjaga lingkungan dan dependensi yang terpisah, semuanya terjadi pada pelari.
  • Anda dapat menguji berbagai cabang secara bersamaan.
  • Anda dapat menguji mulai, berhenti, dan mulai ulang, karena kami dapat mengontrol siklus hidup aplikasi (semuanya dimulai tepat dalam kode uji).
  • Ada HealthChecks fleksibel yang sangat kurang.
  • Tidak ada file * .sh di repositori, Anda dapat mengonfigurasi tes dalam aplikasi ses fleksibel yang Anda inginkan.
  • Berkat Pemetaan classpathResource, Anda dapat menggunakan konfigurasi yang sama dengan tes dan aplikasi.
  • Anda dapat mengonfigurasi tes dari kode.
  • Semua ini berjalan dengan sama mudahnya baik di CI dan lokal, karena ini hanya tes yang terlihat dan dijalankan sebagai unit test, hanya semuanya berjalan dalam wadah buruh pelabuhan.

Ternyata semuanya mencurigakan mulus dan baik, tetapi ini hanya pada pandangan pertama, pada kenyataannya, kami mengalami sejumlah masalah.

Wadah tergantung


Masalah pertama yang kami temui adalah wadah yang tergantung . Katakanlah ada semacam tes:

 class MySpec extends FlatSpec with ForAllTestContainer { val pgCont = PostgreSQLContainer() val appCont = AppContainer(pgCont.jdbcUrl, pgCont.username, pgCont.password) override val container = MultipleContainers(appCont, pgCont) // tests here } 

Ini menjalankan postgres dan AppContainer. AppContainer dari postgres dilewatkan jdbcUrl, nama pengguna dan kata sandi untuk koneksi. Selanjutnya, MultipleContainers dibuat dan tes itu sendiri dijelaskan.

Saya menjalankan program dan melihat kesalahan:

 Exception encountered when invoking run on a nested suite - Mapped port can only be obtained after the container is started 

Intinya adalah bahwa port yang ditugaskan tidak dapat diambil sampai wadah dimulai. Mengapa ini terjadi?

Faktanya adalah bahwa ForAllTestContainer atau ForEachTestContainer memulai kontainer segera sebelum tes, dan tidak pada saat saya membuat instance kontainer. Ternyata saat saya membuat AppContainer, saya belum mengaktifkan PostgreSQLContainer , yang artinya saya tidak bisa mendapatkan port yang ditugaskan darinya, dan diperlukan untuk membentuk jdbcUrl .

Masalahnya adalah bahwa esensi wadah itu bisa berubah: ia memiliki beberapa keadaan. Misalnya, dapat dimatikan dan dihidupkan.

Bagaimana cara mengatasi masalah ini? Metode pertama yang saya sebut "malas."

 class MyTest extends FreeSpec with BeforeAndAfterAll { lazy val pgCont = PostgreSQLContainer() lazy val appCont = AppContainer(pgCont.jdbcUrl, pgCont.username, pgCont.password) override def beforeAll(): Unit = { super.beforeAll() pgCont.start() appCont.start() } override def afterAll(): Unit = { super.afterAll() appCont.stop() pgCont.stop() } // tests here } 

Gagasan utamanya adalah membuat wadah menggunakan val malas . Maka mereka tidak akan segera diinisialisasi dalam konstruktor uji, tetapi akan menunggu panggilan pertama. Kami akan menginisialisasi dalam metode beforeAll dan afterAll , yang disediakan oleh BeforeAndAfterAll dari ScalaTest. Di beforeAll wadah mulai, dan di afterAll , mereka mati. Karena wadah dinyatakan malas, pada saat metode mulai dipanggil sebelum Semua, mereka akan dibuat, diinisialisasi, dan dimulai.

Namun, kesalahan masih terjadi bahwa saya tidak dapat bergabung dengan localhost: 32787:

 org.postgresql.util.PSQLException: Connection to localhost:32787 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. 

Tampaknya kami menggunakan jdbcUrl, mengapa localhost muncul? Mari kita lihat bagaimana jdbcUrl bekerja:

 @Override public String getJdbcUrl() { return "jdbc:postgresql://" + getContainerIpAddress() + ":" + getMappedPort(POSTGRESQL_PORT) + "/" + databaseName; } 

Itu hanya penggabungan string. Semuanya jelas dengan konstanta, mereka tidak dapat rusak. getMappedPort juga berfungsi, karena kami telah memperbaikinya. databaseName adalah konstanta hard-coded. Tetapi dengan getContainerIpAddress lebih menarik. Berdasarkan namanya, kita dapat berasumsi bahwa itu harus mengembalikan alamat IP wadah. Tetapi jika Anda menjalankan kode ini, ternyata selalu mengembalikan localhost. Ternyata, metode ini tidak dimaksudkan untuk interaksi antar wadah: getContainerIpAddress menyediakan interaksi dari tes di dalam wadah .

Rekomendasi pengembang Testcontainers: buat jaringan khusus untuk komunikasi antar wadah . Susunan Docker berfungsi seperti ini: ia membuat jaringan dan menyelesaikan semuanya sendiri.

Jadi, Anda perlu membuat jaringan.

 class MyTest extends FreeSpec with BeforeAndAfterAll { val network: Network = Network.newNetwork() val dbName = "some_db" val pgContainerAlias = "postgres" val jdbcUrl = s"jdbc:postgresql://$pgContainerAlias:5432/$dbName" lazy val pgCont = { val c = PostgreSQLContainer("postgres:9.6") c.container.withNetwork(network) c.container.withNetworkAliases(pgContainerAlias) c.container.withDatabaseName(dbName) c } lazy val appCont = { val c = AppContainer(jdbcUrl, pgCont.username, pgCont.password) c.container.withNetwork(network) c } override def beforeAll(): Unit = { super.beforeAll() pgCont.start() appCont.start() } override def afterAll(): Unit = { super.afterAll() appCont.stop() pgCont.stop() network.close() } // tests here } 

Sekarang kita harus mengkonfigurasi jdbcUrl secara manual. Kita juga perlu mengaktifkan kontainer kita di jaringan, dan mengatur alias untuk PostgreSQLContainer sehingga dapat diakses di dalam jaringan dengan beberapa nama domain. Pada akhirnya, Anda harus ingat untuk "membunuh" jaringan.

Akhirnya, program semacam itu akan berhasil.

Dalam versi terbaru dari testcontainers-scala, inisialisasi wadah malas didukung di luar kotak:

 class MyTest extends FreeSpec with ForAllTestContainer with BeforeAndAfterAll { val network: Network = Network.newNetwork() val dbName = "some_db" val pgContainerAlias = "postgres" val jdbcUrl = s"jdbc:postgresql://$pgContainerAlias:5432/$dbName" lazy val pgCont = { val c = PostgreSQLContainer("postgres:9.6") c.container.withNetwork(network) c.container.withNetworkAliases(pgContainerAlias) c.container.withDatabaseName(dbName) c } lazy val appCont = { val c = AppContainer(jdbcUrl, pgCont.username, pgCont.password) c.container.withNetwork(network) c } override val container = MultipleContainers(pgCont, appCont) override def afterAll(): Unit = { super.afterAll() network.close() } // tests here } 

Anda bisa menggunakan ForAllTestContainer dan MultipleContainers lagi. Di beforeAll tidak perlu lagi beforeAll urutan mulai secara manual. Sekarang MultipleContainers dapat bekerja dengan val malas dan menjalankannya dalam urutan yang benar, dan tidak melakukan inisialisasi yang ketat segera setelah pembuatan. Pada saat yang sama, manipulasi dengan jaringan khusus dan jdbcUrl juga perlu dilakukan secara manual.

Mengejek


Namun, masih ada masalah. Misalnya moki. Terkadang sangat tidak nyaman untuk membuat semacam ketergantungan dalam wadah buruh pelabuhan. Kami menggunakan Spark JobServer, yang menciptakan pekerjaan Spark dan mengontrol siklus hidup mereka di Spark. Kami menggunakan dua metode: "buat" dan "berikan status".

Untuk menjalankan Spark JobServer di dalam docker. Perlu untuk meningkatkan Spark, dan sampai saat ini, tidak memiliki wadah buruh pelabuhan sama sekali dan itu perlu untuk merakit sendiri. Selain itu, Spark JobServer menggunakan PostgreSQL untuk menyimpan status. Akibatnya, Anda harus melakukan banyak pekerjaan sulit ketika Anda benar-benar hanya membutuhkan dua metode dengan API sederhana.

Tetapi Anda dapat mengintip ke dalam implementasi JobServer Spark dan membuat mock yang berperilaku dengan cara yang sama, tetapi tidak memerlukan dependensi dari Spark JobServer yang asli.

Sepertinya ini (dalam contoh, kodesemu yang disederhanakan):

 val hostIp = ??? AppContainer(sparkJobServerMockHost = hostIp) val sparkJobServerMock = new SparkJobServerMock() sparkJobServerMock.init(someData) val apiResult = appApi.callMethod() assert(apiResult == someData) 

http- API Spark JobServer. - , . , , , mock.

- , . : ยซยป config; , host.

SparkJobServerMock , host-, docker-, , , docker-.

? docker-, , gateway , docker-.

, Testcontainers API. , Testcontainers docker-java-, . ยซยป docker-:

 val client: com.github.dockerjava.api.DockerClient = DockerClientFactory .instance .client val networkInfo: com.github.dockerjava.api.model.Network = client .inspectNetworkCmd() .withNetworkId(network.getId) .exec() val hostIp: String = networkInfo .getIpam .getConfig .get(0) .getGateway 

-, DockerClient . Testcontainers DockerClientFactory . c inspectNetworkCmd . , info, gateway.

, , .

โ€” . Docker : Windows, Mac, . Linux. , , Linux .

, Testcontainers . , docker-. :

 Testcontainers.exposeHostPorts(sparkJobServerMockPort) 

, . docker-. `host.testcontainers.internal` .

, :

 val sparkJobServerMockHost = "host.testcontainers.internal" val sparkJobServerMockPort = 33333 Testcontainers.exposeHostPorts(sparkJobServerPort) AppContainer(sparkJobServerMockHost, sparkJobServerMockPort) 


Testcontainers


, , Testcontainers , . Java-, Scala-. :

  • . , testcontainers-java JUnit, testcontainers-scala ScalaTest, testcontainers-java . Scala- .
  • Scala . . , . , predefined Java-. , .
  • API . API, . , . , , .

Ringkasan


. Docker , , , , network gateway.

Testcontainers โ€” , . API , .

Java-, . โ€” . .

, docker-, .

โ€” , , , . .?

, .

โ€” - ?

Kubernetes, . end-to-end , , , , .

, , unit-, .

โ€” Kubernetes ?

-, , -, , , , Spark Kubernetes ; , .

, , unit-, , , break point , , .

, , , CI , .

, minicube โ€” Mac, . , , , , .

โ€” ? : master? , - , , 2.1, 2.2, ?

ImageName, Postgres 9.6.

 val pgContainer: PostgreSQLContainer = PostgreSQLContainer("postgres:9.6") 

9.6, 10. [ ], .

Image tag โ€” , โ€” , . , latest .

โ€” , ?

, CI , GitLab CI , , Branch Name.

โ€” , , , ? - , ? 20- , ?

-, , . , , , , , .

- , , full-time , , , .

commit', , , , Android, iOS . . , , , , โ€” .

, , -: - , - . , - .

Ingin lebih detail tentang layanan microser sendiri dan tidak hanya pada Scala - program ScalaConf kami memiliki jawaban untuk berbagai pertanyaan. Lebih tertarik pada arsitektur dan interkoneksi berbagai bagiannya - datang ke HighLoad ++ pada 7-8 November.

Semuanya sangat lezat, dan tidak jelas apa yang harus dipilih, kemudian berlangganan buletin di mana kita berbicara tentang laporan dan mengumpulkan bahan-bahan yang berguna tentang topik tersebut.

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


All Articles