Tes yang ditulis dengan baik secara signifikan mengurangi risiko "merusak" aplikasi saat menambahkan fitur baru atau memperbaiki bug. Dalam sistem kompleks yang terdiri dari beberapa komponen yang saling berhubungan, yang paling sulit adalah menguji landasan bersama mereka.
Pada artikel ini, saya akan berbicara tentang bagaimana kami menemui kesulitan dalam menulis tes yang baik ketika mengembangkan komponen on Go dan bagaimana kami memecahkan masalah ini menggunakan perpustakaan RSpec di Ruby on Rails.
Menambahkan Pergi ke tumpukan teknologi proyek
Salah satu proyek yang sedang dikembangkan eTeam, tempat saya bekerja, dapat dibagi menjadi: panel admin, akun pengguna, pembuat laporan dan memproses permintaan dari berbagai layanan yang kami integrasikan.
Bagian yang bertanggung jawab untuk memproses permintaan adalah yang paling penting, jadi saya ingin membuatnya seandal dan semurah mungkin. Menjadi bagian dari aplikasi monolitik, ia berisiko mendapatkan bug ketika mengubah bagian kode yang tidak terkait dengannya. Ada juga risiko menjatuhkan pemrosesan saat memuat komponen aplikasi lainnya. Jumlah pekerja Ngnix per aplikasi terbatas, dan saat beban bertambah, misalnya, membuka banyak halaman berat di panel admin, pekerja bebas berhenti dan pemrosesan permintaan melambat, atau bahkan turun.
Risiko-risiko ini, serta kematangan sistem ini (selama berbulan-bulan tidak harus melakukan perubahan) menjadikannya kandidat yang ideal untuk pemisahan ke dalam layanan terpisah.
Diputuskan untuk menulis layanan terpisah ini di Go. Dia harus berbagi akses ke database dengan aplikasi Rails. Tanggung jawab untuk kemungkinan perubahan pada struktur tabel tetap dengan Rails. Pada prinsipnya, skema seperti itu dengan database umum berfungsi dengan baik, sementara hanya ada dua aplikasi. Itu terlihat seperti ini:

Layanan ini ditulis dan digunakan untuk instance yang terpisah dari Rails. Sekarang, saat memasang aplikasi Rails, Anda tidak perlu khawatir itu akan memengaruhi pemrosesan kueri. Layanan menerima permintaan HTTP secara langsung, tanpa Ngnix, menggunakan sedikit memori, sedikit banyak bersifat minimalis.
Masalah dengan tes unit kami di Go
Tes unit diimplementasikan dalam aplikasi Go, dan semua kueri basis data di dalamnya dikunci. Di antara argumen lain yang mendukung solusi tersebut adalah sebagai berikut: aplikasi Rails utama bertanggung jawab untuk struktur database, sehingga aplikasi-go tidak "memiliki" informasi untuk membuat database pengujian. Memproses permintaan untuk setengah terdiri dari logika bisnis dan setengah dari bekerja dengan database, dan setengah ini benar-benar dikunci. Moki in Go terlihat kurang "terbaca" daripada di Ruby. Saat menambahkan fungsi baru untuk membaca data dari database, itu diperlukan untuk menambahkan moki untuk itu di set tes jatuh yang bekerja sebelumnya. Akibatnya, unit test semacam itu tidak efektif dan sangat rapuh.
Metode solusi
Untuk menghilangkan kekurangan ini, diputuskan untuk mencakup layanan dengan tes fungsional yang terletak di aplikasi Rails dan menguji layanan on Go sebagai kotak hitam. Sebagai kotak putih, masih tidak akan berfungsi, karena dari ruby, bahkan dengan semua keinginan, tidak mungkin untuk campur tangan dalam layanan, misalnya, mendapatkan beberapa metode basah untuk memeriksa apakah itu dipanggil. Ini juga berarti bahwa permintaan yang dikirim oleh layanan yang diuji juga tidak dapat dikunci, oleh karena itu, aplikasi lain diperlukan untuk menangkap dan mencatatnya. Sesuatu seperti RequestBin, tetapi lokal. Kami sudah menulis utilitas serupa, jadi kami menggunakannya.
Skema berikut telah berubah:
- rspec mengkompilasi dan memulai layanan saat bepergian, memberikannya konfigurasi, yang berisi akses ke basis tes dan port tertentu untuk menerima permintaan HTTP, misalnya 8082
- sebuah utilitas juga diluncurkan untuk mencatat permintaan HTTP yang diterima di port 8083
- kami menulis tes biasa pada RSpec, mis. buat data yang diperlukan dalam database dan kirim permintaan ke localhost: 8082, seolah-olah ke layanan eksternal, misalnya menggunakan HTTParty
- respon parsim; periksa perubahan dalam basis data; kami mendapatkan daftar permintaan yang direkam dari "RequestBin" dan memeriksanya.
Detail Implementasi:
Sekarang tentang bagaimana penerapannya. Untuk tujuan demonstrasi, beri nama layanan yang diuji: "Layanan" dan buat pembungkus untuk itu:
Untuk berjaga-jaga, saya akan membuat reservasi bahwa dalam Rspec itu harus dikonfigurasi untuk memuat file secara otomatis dari folder "support":
Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}
Metode awal:
- membaca dari konfigurasi terpisah path ke sumber TheService dan informasi yang diperlukan untuk dijalankan. Karena informasi ini mungkin berbeda dari pengembang yang berbeda, konfigurasi ini dikecualikan dari Git. Konfigurasi yang sama berisi pengaturan yang diperlukan untuk program yang akan diluncurkan. Konfigurasi heterogen ini terletak di satu tempat agar tidak menghasilkan file tambahan.
- mengkompilasi dan menjalankan program melalui “go run {path to main.go} {path to config}”
- polling setiap detik, menunggu sampai program yang berjalan siap untuk menerima permintaan
- ingat pengenal proses agar tidak memulai kembali dan dapat menghentikannya.
konfigurasi sendiri:
#/spec/support/the_service_config.yml server: addr: 127.0.0.1:8082 db: dsn: dbname=project_test sslmode=disable user=postgres password=secret redis: url: redis://127.0.0.1:6379/1 rails: main_go: /home/me/go/src/github.com/company/theservice/main.go recorder_addr: 127.0.0.1:8083 env: PATH: '/home/me/.gvm/gos/go1.10.3/bin' GOROOT: '/home/me/.gvm/gos/go1.10.3' GOPATH: '/home/me/go'
Metode berhenti hanya menghentikan proses. Hal baru adalah bahwa ruby menjalankan perintah "Go Run" yang menjalankan biner yang dikompilasi dalam proses anak yang ID-nya tidak diketahui. Jika Anda baru saja menghentikan proses mulai dari ruby, maka proses anak tidak secara otomatis berhenti dan port tetap sibuk. Oleh karena itu, penghentian terjadi oleh ID Grup Proses:
Sekarang kita akan menyiapkan konteks shared_context di mana kita mendefinisikan variabel default, mulai Layanan jika belum dimulai, dan sementara menonaktifkan VCR (dari sudut pandangnya, kita berbicara dengan layanan eksternal, tetapi bagi kita sekarang tidak begitu):
dan sekarang Anda dapat mulai menulis spesifikasi sendiri:
Layanan dapat membuat permintaan HTTPnya ke layanan eksternal. Menggunakan konfigurasi, kami mengarahkan ulang ke utilitas lokal yang menulisnya. Ada juga pembungkus untuk memulai dan berhenti, ini mirip dengan kelas "TheServiceControl", kecuali bahwa utilitas hanya dapat dimulai tanpa kompilasi.
Roti ekstra
Aplikasi Go ditulis sehingga semua log dan informasi debug ditampilkan di STDOUT. Ketika diluncurkan dalam produksi, output ini dikirim ke file. Dan ketika diluncurkan dari Rspec, ini ditampilkan di konsol, yang sangat membantu ketika melakukan debugging.
Jika spesifikasi dijalankan secara selektif, di mana Layanan tidak diperlukan, maka spesifikasi tersebut tidak dimulai.
Untuk menghindari buang waktu mengembangkan layanan setiap kali Anda me-restart spesifikasi ketika mengembangkan, Anda dapat memulai layanan secara manual di terminal dan tidak mematikannya. Jika perlu, Anda bahkan dapat menjalankannya dalam IDE dalam mode debug, dan kemudian spec akan menyiapkan semua yang Anda butuhkan, mengajukan permintaan untuk layanan, itu akan berhenti dan Anda dapat merendahkan diri tanpa repot. Ini membuat pendekatan TDD sangat nyaman.
Kesimpulan
Skema semacam itu telah bekerja selama sekitar satu tahun dan tidak pernah gagal. Spesifikasi jauh lebih mudah dibaca daripada tes unit on Go, dan tidak bergantung pada pengetahuan tentang struktur internal layanan. Jika, karena alasan tertentu, kami perlu menulis ulang layanan dalam bahasa lain, kami tidak perlu mengubah spesifikasi, kecuali untuk pembungkus, yang hanya perlu memulai layanan pengujian dengan perintah lain.