
Nama saya Vadim, saya adalah pengembang terkemuka di Pencarian Mail.Ru. Saya akan membagikan pengalaman kami dalam pengujian unit. Artikel ini terdiri dari tiga bagian: pada bagian pertama saya akan memberi tahu Anda apa yang umumnya kami capai dengan bantuan pengujian unit; bagian kedua menjelaskan prinsip-prinsip yang kita ikuti; dan dari bagian ketiga Anda akan belajar bagaimana prinsip-prinsip yang disebutkan diimplementasikan dalam Python.
Tujuan
Sangat penting untuk memahami mengapa Anda menerapkan pengujian unit. Tindakan nyata akan tergantung pada ini. Jika Anda menggunakan unit test secara tidak benar, atau dengan bantuan mereka Anda tidak melakukan apa yang Anda inginkan, maka tidak ada hal baik yang akan terjadi. Karena itu, sangat penting untuk memahami terlebih dahulu tujuan apa yang Anda kejar.
Dalam proyek kami, kami mengejar beberapa tujuan.
Yang pertama adalah
regresi dangkal: untuk memperbaiki sesuatu dalam kode, menjalankan tes dan menemukan bahwa tidak ada yang rusak. Meskipun pada kenyataannya tidak sesederhana kedengarannya.
Tujuan kedua adalah
mengevaluasi dampak arsitektur . Jika Anda memperkenalkan pengujian unit wajib dalam proyek, atau cukup setuju dengan pengembang tentang penggunaan tes unit, ini akan segera mempengaruhi gaya penulisan kode. Tidak mungkin menulis fungsi pada 300 baris dengan 50 variabel lokal dan 15 parameter jika fungsi-fungsi ini dikenai pengujian unit. Selain itu, berkat pengujian ini, antarmuka akan menjadi lebih mudah dipahami dan beberapa area masalah akan muncul. Lagi pula, jika kodenya tidak begitu panas, maka tes akan menjadi kurva, dan itu akan segera menarik perhatian Anda.
Tujuan ketiga adalah
membuat kode lebih jelas . Misalkan Anda datang ke proyek baru dan diberi 50 MB kode sumber. Anda mungkin tidak bisa mengetahuinya. Jika tidak ada tes unit, maka satu-satunya cara untuk berkenalan dengan pekerjaan kode, selain membaca sumbernya, adalah "metode poke". Tetapi jika sistemnya cukup rumit, maka perlu banyak waktu untuk mendapatkan potongan kode yang diperlukan melalui antarmuka. Dan berkat unit test, Anda dapat melihat bagaimana kode dieksekusi dari mana saja.
Tujuan keempat adalah
menyederhanakan debugging . Misalnya, Anda telah menemukan beberapa kelas dan ingin men-debugnya. Jika alih-alih pengujian unit hanya ada tes sistem, atau tidak ada tes sama sekali, maka tes ini hanya berjalan ke tempat yang tepat melalui antarmuka. Saya kebetulan berpartisipasi dalam proyek di mana, untuk menguji beberapa fitur, butuh setengah jam untuk membuat pengguna, menagih uang kepadanya, mengubah statusnya, meluncurkan semacam cron, sehingga status ini ditransfer ke tempat lain, lalu mengklik sesuatu di antarmuka, meluncurkan sesuatu beberapa cron lainnya ... Setelah setengah jam, program bonus untuk pengguna ini akhirnya muncul. Dan jika saya memiliki unit test, maka saya bisa segera sampai ke tempat yang tepat.
Akhirnya, tujuan yang paling penting dan sangat abstrak, yang menyatukan semua yang sebelumnya, adalah
kenyamanan . Ketika saya memiliki unit test, saya mengalami lebih sedikit stres ketika bekerja dengan kode, karena saya mengerti apa yang terjadi. Saya dapat mengambil sumber yang tidak dikenal, memperbaiki tiga baris, menjalankan tes dan memastikan bahwa kode berfungsi sebagaimana dimaksud. Dan bahkan tesnya tidak hijau: bisa merah, tapi persis seperti yang saya harapkan. Yaitu, saya mengerti bagaimana kodenya bekerja.
Prinsip
Jika Anda memahami tujuan Anda, Anda dapat memahami apa yang perlu dilakukan untuk mencapainya. Dan di sini masalahnya dimulai. Faktanya adalah bahwa banyak buku dan artikel telah ditulis pada unit testing, tetapi teorinya masih sangat belum matang.
Jika Anda pernah membaca artikel tentang pengujian unit, mencoba menerapkan yang dijelaskan dan Anda tidak berhasil, maka sangat mungkin bahwa alasannya adalah ketidaksempurnaan teori. Ini terjadi setiap saat. Saya, seperti semua pengembang, pernah berpikir bahwa masalahnya ada pada saya. Dan kemudian dia menyadari: tidak mungkin saya salah berkali-kali. Dan dia memutuskan bahwa dalam unit testing itu perlu untuk melanjutkan dari pertimbangannya sendiri, untuk bertindak lebih masuk akal.
Saran standar yang dapat Anda temukan di semua buku dan artikel: "Anda harus menguji bukan implementasinya, tetapi antarmuka". Bagaimanapun, implementasinya dapat berubah, tetapi antarmuka tidak bisa. Mari kita mengujinya sehingga tes tidak jatuh setiap saat pada setiap kesempatan. Nasehatnya, sepertinya, tidak buruk, dan semuanya tampak logis. Tapi kami tahu betul: untuk menguji sesuatu, Anda perlu memilih beberapa nilai tes. Biasanya, saat menguji fungsi, yang disebut kelas ekivalensi dibedakan: himpunan nilai di mana fungsi berperilaku seragam. Secara kasar, tes untuk masing-masing jika. Tetapi untuk mengetahui kelas kesetaraan yang kita miliki, sebuah implementasi diperlukan. Anda tidak mengujinya, tetapi Anda membutuhkannya, Anda harus memeriksanya untuk mengetahui nilai tes mana yang harus dipilih.
Bicaralah dengan penguji mana pun: ia akan memberi tahu Anda bahwa dengan pengujian manual ia selalu membayangkan implementasi. Dari pengalamannya, ia sangat mengerti di mana programmer biasanya membuat kesalahan. Penguji tidak memeriksa semuanya, pertama memasukkan 5, lalu 6, lalu 7. Dia memeriksa 5, abc, โ7, dan jumlahnya 100 karakter, karena dia tahu bahwa implementasi untuk nilai-nilai ini mungkin berbeda, tetapi untuk 6 dan 7 tidak mungkin .
Jadi tidak jelas bagaimana mengikuti prinsip "uji antarmuka, bukan implementasinya." Anda tidak bisa mengambil, memejamkan mata, dan menulis ujian. TDD sedang mencoba menyelesaikan masalah ini sebagian. Teori ini menyarankan untuk memperkenalkan kelas kesetaraan satu per satu dan menulis tes untuk mereka. Saya telah membaca banyak buku dan artikel tentang hal ini, tetapi entah bagaimana itu tidak menempel. Namun, saya setuju dengan tesis bahwa tes harus ditulis terlebih dahulu. Kami menyebut tes prinsip ini terlebih dahulu. Kami tidak memiliki TDD, dan sehubungan dengan hal di atas, tes tidak ditulis sebelum kode dibuat, tetapi bersamaan dengan itu.
Saya pasti tidak merekomendasikan tes menulis surut. Lagi pula, mereka memengaruhi arsitektur, dan jika arsitekturnya sudah mapan, maka sudah terlambat untuk mempengaruhinya - semuanya harus ditulis ulang. Dengan kata lain, testability kode adalah properti terpisah yang harus diberikan oleh kode, tidak akan menjadi seperti itu. Karena itu, kami mencoba menulis tes bersama dengan kode. Jangan percaya pada cerita seperti "mari kita menulis proyek dalam tiga bulan, dan kemudian tutupi semuanya dengan tes dalam seminggu", ini tidak akan pernah terjadi.
Hal yang paling penting untuk dipahami: pengujian unit bukanlah cara untuk memverifikasi kode, bukan cara untuk memverifikasi kebenarannya. Ini adalah bagian dari arsitektur Anda, desain aplikasi Anda. Ketika Anda bekerja dengan unit test, Anda mengubah kebiasaan Anda. Tes yang hanya memverifikasi kebenaran adalah tes penerimaan. Ini akan menjadi kesalahan untuk berpikir bahwa Anda kemudian dapat menutupi sesuatu dengan tes unit, atau bahwa kode tidak perlu diperiksa.
Implementasi python
Kami menggunakan pustaka unittest standar dari keluarga xUnit. Ceritanya begini: ada bahasa SmallTalk, dan di dalamnya ada perpustakaan SUnit. Semua orang menyukainya, mereka mulai menyalinnya. Perpustakaan diimpor ke Jawa dengan nama Junit, dari sana di C ++ dengan nama CppUnit dan ke Ruby dengan nama RUnit (kemudian diubah namanya menjadi RSpec). Akhirnya, dari Jawa, perpustakaan "pindah" ke Python dengan nama unittest. Dan mereka mengimpornya secara harfiah sehingga CamelCase tetap ada, meskipun ini tidak sesuai dengan PEP 8.
Tentang xUnit ada buku yang bagus, "xUnit Test Patterns". Ini menjelaskan cara bekerja dengan kerangka kerja keluarga ini. Satu-satunya kelemahan dari buku ini adalah ukurannya: itu besar, tetapi sekitar 2/3 dari isinya adalah katalog pola. Dan sepertiga pertama dari buku ini sangat luar biasa, ini adalah salah satu buku terbaik tentang TI yang saya temui.
Tes unit adalah kode reguler yang memiliki arsitektur standar tertentu. Semua tes unit terdiri dari tiga tahap: pengaturan, latihan dan verifikasi. Anda menyiapkan data, menjalankan tes dan melihat apakah semuanya telah mencapai kondisi yang tepat.

Pengaturan
Tahap paling sulit dan menarik. Membawa sistem ke kondisi semula tempat Anda ingin mengujinya bisa sangat sulit. Dan keadaan sistem bisa rumit semena-mena.
Pada saat fungsi Anda dipanggil, banyak peristiwa bisa terjadi, sejuta objek bisa saja dibuat dalam memori. Dalam semua komponen yang terkait dengan perangkat lunak Anda - dalam sistem file, database, cache - ada sesuatu yang sudah ditemukan, dan fungsinya hanya dapat berfungsi di lingkungan ini. Dan jika lingkungan tidak siap, maka tindakan fungsi tidak akan berarti.
Biasanya setiap orang mengklaim bahwa Anda tidak dapat menggunakan sistem file, database, atau komponen terpisah lainnya, karena ini membuat pengujian Anda bukan modular, tetapi integrasi. Menurut pendapat saya, ini tidak benar, karena tes integrasi dilakukan oleh tes integrasi. Jika Anda menggunakan beberapa komponen bukan untuk verifikasi, tetapi hanya untuk membuat sistem bekerja, tidak ada yang salah dengan itu. Kode Anda berinteraksi dengan banyak komponen komputer dan OS. Satu-satunya masalah dengan menggunakan sistem file atau database adalah kecepatan.
Langsung dalam kode, kami menggunakan
injeksi ketergantungan . Anda dapat membuang parameter ke dalam fungsi alih-alih yang default. Anda bahkan dapat meneruskan tautan ke perpustakaan. Atau Anda dapat menyelipkan rintisan alih-alih permintaan sehingga kode dari tes tidak mengakses jaringan. Anda dapat menyimpan logger khusus di atribut kelas agar tidak menulis ke disk dan menghemat waktu.
Untuk bertopik, kami menggunakan mock biasa dari unittest. Ada juga fungsi tambalan yang, alih-alih secara jujur โโmenerapkan dependensi, hanya mengatakan: "dalam paket ini, impor adalah pengganti yang lain." Ini nyaman karena Anda tidak perlu membuang apa pun di mana pun. Benar, maka tidak jelas siapa yang menggantikan apa, jadi gunakan dengan hati-hati.
Sedangkan untuk sistem file, maka pemalsuan itu cukup sederhana. Ada modul io dengan
io.StringIO
dan
io.BytesIO
. Anda dapat membuat objek seperti file yang tidak benar-benar mengakses disk. Tetapi jika tiba-tiba ini tidak cukup untuk Anda, maka ada modul tempfile yang hebat dengan manajer konteks untuk file sementara, direktori, file bernama, apa saja. Tempfile adalah supermodule jika karena alasan tertentu IO tidak cocok untuk Anda.
Dengan database, semuanya lebih rumit. Ada rekomendasi standar: "Gunakan bukan basis yang nyata, tetapi palsu." Saya tidak tahu tentang Anda, tetapi dalam hidup saya, saya belum melihat satu pun basis yang palsu dan cukup fungsional. Setiap kali saya meminta saran tentang apa yang harus diambil di bawah Python atau Perl, mereka menjawab bahwa tidak ada yang tahu apa-apa siap, dan menawarkan untuk menulis sesuatu dari mereka sendiri. Saya tidak bisa membayangkan bagaimana Anda dapat menulis emulator, misalnya, PostgreSQL. Kiat lain: "lalu dapatkan SQLite." Tetapi ini akan memecah isolasi, karena SQLite bekerja dengan sistem file. Selain itu, jika Anda menggunakan sesuatu seperti MySQL atau PostgreSQL, maka SQLite mungkin tidak akan berfungsi. Jika menurut Anda Anda tidak menggunakan kemampuan khusus produk tertentu, maka kemungkinan besar Anda salah. Tentunya bahkan untuk hal-hal biasa, seperti bekerja dengan tanggal, Anda menggunakan fitur spesifik yang hanya didukung oleh DBMS Anda.
Akibatnya, mereka biasanya menggunakan basis nyata. Solusinya tidak buruk, hanya saja kita perlu menunjukkan tingkat akurasi tertentu. Jangan gunakan database terpusat, karena tes dapat pecah di antara mereka sendiri. Idealnya, pangkalan itu sendiri harus naik selama pengujian dan berhenti setelah pengujian.
Situasi yang sedikit lebih buruk adalah ketika Anda diminta untuk menjalankan database lokal, yang akan digunakan. Tetapi pertanyaannya adalah, bagaimana data akan sampai di sana? Kami telah mengatakan bahwa harus ada keadaan awal sistem, harus ada beberapa data dalam database. Dari mana mereka berasal bukanlah pertanyaan yang mudah.
Pendekatan paling naif yang pernah saya temui adalah dengan menggunakan salinan database nyata. Salinan secara teratur diambil darinya, dari mana data sensitif dihapus. Para penulis beralasan bahwa data nyata paling cocok untuk pengujian. Plus, menulis tes untuk salinan database nyata adalah siksaan. Anda tidak tahu data apa yang ada. Anda harus terlebih dahulu menemukan apa yang akan Anda uji. Jika informasi ini tidak ada, maka apa yang harus dilakukan tidak jelas. Akhirnya dalam proyek itu mereka memutuskan untuk menulis tes untuk akun departemen operasi, yang "tidak akan pernah berubah". Tentu saja, setelah beberapa waktu dia berubah.
Ini biasanya diikuti oleh keputusan: โmari kita membuat para pemain dari basis nyata, menyalinnya dan tidak lagi menyinkronkan. Maka akan mungkin untuk diikat ke objek tertentu, menonton apa yang terjadi di sana dan menulis tes. " Pertanyaan segera muncul: apa yang akan terjadi ketika tabel baru ditambahkan ke database? Tampaknya, Anda harus memasukkan data palsu secara manual.
Tapi karena kita akan tetap melakukannya, mari kita segera menyiapkan cast dasar secara manual. Opsi ini sangat mirip dengan apa yang biasanya disebut fixture di Django: mereka membuat JSON besar, mengunggah kasus uji untuk semua kesempatan, mengirimnya ke database pada awal pengujian, dan semuanya akan baik-baik saja dengan kami. Pendekatan ini juga memiliki banyak kelemahan. Data ditumpuk dalam tumpukan, tidak jelas tes apa yang berkaitan dengannya. Tidak ada yang bisa mengerti apakah data itu dihapus atau tidak dihapus. Dan ada kondisi database yang tidak kompatibel: misalnya, satu tes tidak perlu memiliki pengguna dalam database, dan yang lain untuk memilikinya. Kedua kondisi ini tidak dapat disimpan secara bersamaan dalam cetakan yang sama. Dalam hal ini, salah satu tes harus memodifikasi database. Dan karena Anda masih harus berurusan dengan ini, paling mudah untuk memulai dari database kosong, sehingga setiap tes menempatkan data yang diperlukan di sana, dan pada akhir pengujian membersihkan database. Satu-satunya kelemahan dari pendekatan ini adalah sulitnya membuat data dalam setiap tes. Dalam salah satu proyek tempat saya bekerja, untuk membuat layanan, perlu untuk menghasilkan 8 entitas dalam tabel yang berbeda: layanan pada akun pribadi, akun pribadi pada klien, klien pada entitas hukum, entitas hukum di kota, klien di kota, dan sebagainya. Sampai Anda membuat semua ini dalam sebuah rantai, Anda tidak akan memuaskan kunci asing, tidak ada yang berhasil.
Untuk situasi seperti itu, ada perpustakaan khusus yang sangat memudahkan kehidupan. Anda dapat menulis alat bantu, biasanya disebut pabrik (jangan bingung dengan pola desain). Misalnya, kami menggunakan pustaka factory_boy, yang cocok untuk Django. Ini adalah tiruan dari perpustakaan factory_girl, yang diganti namanya factory_bot tahun lalu karena alasan kebenaran politik. Menulis perpustakaan seperti itu untuk kerangka kerja Anda sendiri tidak memerlukan biaya apa pun. Ini didasarkan pada ide yang sangat penting: Anda pernah membuat pabrik untuk objek yang ingin Anda spawn, membangun koneksi untuknya, dan kemudian memberi tahu pengguna: "ketika Anda dibuat, ambil nama depan Anda, dan hasilkan grup sendiri menggunakan pabrik grup". Dan di pabrik, semuanya persis sama: menghasilkan nama sedemikian rupa, entitas terkait ini dan itu.
Akibatnya, hanya satu baris terakhir yang tersisa dalam kode:
user = UserFactory()
. Pengguna telah dibuat, dan Anda dapat bekerja dengannya, karena di bawah kap ia menghasilkan semua yang diperlukan. Jika mau, Anda dapat mengonfigurasi sesuatu secara manual.
Untuk membersihkan data setelah pengujian, kami menggunakan transaksi sepele. Pada awal setiap tes, MULAI dilakukan, tes melakukan sesuatu dengan basis, dan setelah tes, ROLLBACK dilakukan. Jika transaksi diperlukan dalam tes itu sendiri - misalnya, karena melakukan sesuatu yang ekstra ke database - itu memanggil metode yang kita sebut
break_db
, memberi tahu kerangka kerja bahwa itu merusak database, dan kerangka kerja menggulung ulang itu. Ternyata lambat, tetapi karena biasanya ada sangat sedikit tes yang membutuhkan transaksi, semuanya beres.
Latihan
Tidak ada yang istimewa untuk diceritakan tentang tahap ini. Satu-satunya hal yang mungkin salah di sini adalah beralih ke luar, misalnya, ke Internet. Untuk beberapa waktu, kami berjuang dengan ini secara administratif: kami mengatakan kepada programmer bahwa kami harus mencelupkan fungsi yang pergi ke suatu tempat atau melempar bendera khusus sehingga fungsi tidak. Jika tes mengakses perusahaan dll, ini tidak baik. Akibatnya, kami sampai pada kesimpulan bahwa semuanya sia-sia: kita sendiri terus-menerus lupa bahwa beberapa fungsi memanggil fungsi yang memanggil fungsi yang masuk ke etcd. Oleh karena itu, dalam pengaturan kelas dasar, kami menambahkan moki semua panggilan, yaitu, diblokir dengan bantuan stubs semua panggilan yang tidak dimasukkan.
Rintisan bertopik dapat dengan mudah dibuat menggunakan patcher, menempatkan patcher dalam kamus terpisah dan memberikan akses ke semua tes. Secara default, tes tidak dapat pergi ke mana pun, dan jika untuk beberapa Anda masih perlu membuka akses, Anda dapat mengarahkan ulang. Sangat nyaman Jenkins tidak akan lagi mengirim SMS ke pelanggan Anda di malam hari :)
Verifikasi
Pada tahap ini, kami secara aktif menggunakan pernyataan tertulis, bahkan satu baris. Jika Anda menguji keberadaan file dalam tes, maka alih-alih menegaskan
self.assertTrue(file_exists(f))
sarankan menulis menegaskan
not file exists
. Holivar terhubung dengan ini: haruskah saya terus menggunakan CamelCase dalam nama, seperti di unittest, atau haruskah saya mengikuti PEP 8? Saya tidak punya jawaban. Jika Anda mengikuti PEP 8, maka dalam kode uji akan ada kekacauan dari CamelCase dan snake_case. Dan jika Anda menggunakan CamelCase, maka ini tidak sesuai dengan PEP 8.
Dan yang terakhir. Misalkan Anda memiliki kode yang menguji sesuatu, dan ada banyak opsi data yang harus dijalankan oleh kode ini. Jika Anda menggunakan py.test, di sana Anda dapat menjalankan tes yang sama dengan data input yang berbeda. Jika Anda tidak memiliki py.test, maka Anda dapat menggunakan
dekorator seperti itu . Sebuah meja dilewatkan ke dekorator, dan satu tes berubah menjadi beberapa lainnya, masing-masing menguji salah satu kasing.
Kesimpulan
Jangan percaya artikel dan buku tanpa syarat. Jika Anda berpikir bahwa mereka salah, mungkin saja memang demikian.
Jangan ragu untuk menggunakan tes ketergantungan. Tidak ada yang salah dengan itu. Jika Anda mengangkat memcached, karena tanpanya kode Anda tidak berfungsi secara normal, tidak apa-apa. Tetapi lebih baik melakukannya tanpa itu, jika memungkinkan.
Perhatikan pabriknya. Ini adalah pola yang sangat menarik.
PS Saya mengundang Anda ke saluran Telegram penulis saya untuk pemrograman dengan Python - @pythonetc.