Cara berhenti khawatir dan mulai menulis tes berbasis properti

Baru-baru ini, semakin sering ada referensi ke alat magis tertentu - pengujian berdasarkan properti (pengujian berbasis properti, jika Anda perlu google literatur bahasa Inggris). Sebagian besar artikel tentang topik ini berbicara tentang apa itu pendekatan yang keren, kemudian mereka menunjukkan pada contoh dasar bagaimana menulis tes seperti itu menggunakan kerangka kerja tertentu, paling baik mereka menyarankan beberapa sifat umum, dan ... itu saja. Lebih jauh, pembaca yang kagum dan antusias mencoba mempraktekkan semua ini, dan bersandar pada fakta bahwa properti entah bagaimana tidak ditemukan. Dan sayangnya, sering menyerah untuk ini. Pada artikel ini saya akan mencoba memprioritaskan sedikit berbeda. Tetap saja, saya akan mulai dengan contoh yang kurang lebih konkret untuk menjelaskan jenis hewan apa itu. Tapi sebuah contoh, saya harap, tidak cukup khas untuk artikel semacam ini. Lalu saya akan mencoba untuk menganalisis beberapa masalah yang terkait dengan pendekatan ini, dan bagaimana mereka dapat diselesaikan. Dan selanjutnya - properti, properti dan hanya properti, dengan contoh di mana mereka dapat didorong. Menarik?

Menguji penyimpanan nilai kunci dalam tiga tes singkat


Jadi, katakanlah karena alasan tertentu kita perlu menerapkan semacam penyimpanan nilai kunci. Ini bisa berupa kamus berdasarkan tabel hash, atau berdasarkan pada beberapa pohon, dapat sepenuhnya disimpan dalam memori, atau dapat bekerja dengan disk - kami tidak peduli. Hal utama adalah bahwa ia harus memiliki antarmuka yang memungkinkan Anda untuk:

  • tulis nilai dengan kunci
  • periksa apakah ada entri dengan kunci yang diinginkan
  • baca nilai dengan kunci
  • dapatkan daftar barang yang direkam
  • dapatkan salinan repositori

Dalam pendekatan berbasis contoh klasik, tes khas akan terlihat seperti ini:

storage = Storage() storage['a'] = 42 assert len(storage) == 1 assert 'a' in storage assert storage['a'] == 42 

Atau lebih:

 storage = Storage() storage['a'] = 42 storage['b'] = 73 assert len(storage) == 2 assert 'a' in storage assert 'b' in storage assert storage['a'] == 42 assert storage['b'] == 73 

Dan secara umum, tes seperti itu dapat dan perlu ditulis sedikit lebih dari dofiga. Selain itu, semakin rumit implementasi internal, semakin banyak peluang untuk melewatkan sesuatu. Singkatnya, pekerjaan yang panjang, melelahkan, dan seringkali tanpa rasa terima kasih. Betapa menyenangkannya mendorong seseorang! Sebagai contoh, buat komputer menghasilkan test case untuk kami. Pertama, coba lakukan sesuatu seperti ini:

 storage = Storage() key = arbitrary_key() value = arbitrary_value() storage[key] = value assert len(storage) == 1 assert key in storage assert storage[key] == value 

Ini adalah tes berbasis properti pertama. Itu terlihat hampir sama dengan yang tradisional, meskipun bonus kecil sudah mencolok - tidak ada nilai yang diambil dari langit-langit di dalamnya, alih-alih kami menggunakan fungsi yang mengembalikan kunci dan nilai sewenang-wenang. Ada keuntungan lain, yang jauh lebih serius - ini dapat dilakukan berkali-kali dan pada data input yang berbeda untuk memeriksa kontrak bahwa jika Anda mencoba menambahkan beberapa elemen ke penyimpanan kosong, maka itu benar-benar akan ditambahkan di sana. Oke, itu semua baik dan bagus, tapi sejauh ini tidak terlalu berguna dibandingkan dengan pendekatan tradisional. Mari kita coba tambahkan tes lain:

 storage = arbitrary_storage() storage_copy = storage.copy() assert len(storage) == len(storage_copy) assert all(storage_copy[key] == storage[key] for key in storage) assert all(storage[key] == storage_copy[key] for key in storage_copy) 

Di sini, alih-alih mengambil penyimpanan kosong, kami menghasilkan secara acak dengan beberapa data, dan memeriksa apakah salinannya identik dengan yang asli. Ya, generator perlu ditulis menggunakan API publik yang berpotensi bermasalah, tetapi sebagai aturan ini bukan tugas yang sulit. Pada saat yang sama, jika ada bug serius dalam implementasi, maka kemungkinan besar bahwa kejatuhan akan mulai selama proses pembuatan, jadi ini juga dapat dianggap sebagai semacam tes bonus asap. Tetapi sekarang kita dapat yakin bahwa semua yang dapat disediakan oleh generator dapat disalin dengan benar. Dan berkat pengujian pertama, kami tahu pasti bahwa generator dapat membuat penyimpanan dengan setidaknya satu elemen. Waktunya untuk ujian selanjutnya! Pada saat yang sama, kami menggunakan kembali generator:

 storage = arbitrary_storage() backup = storage.copy() key = arbitrary_key() value = arbitrary_value() if key in storage: return storage[key] = value assert len(storage) == len(backup) + 1 assert key in storage assert storage[key] == value assert all(storage[key] == backup[key] for key in backup) 

Kami mengambil penyimpanan sewenang-wenang, dan memeriksa apakah kami dapat menambahkan elemen lain di sana. Jadi generator dapat membuat repositori dengan dua elemen. Dan Anda dapat menambahkan elemen ke dalamnya juga. Dan seterusnya (saya langsung ingat hal seperti induksi matematika). Sebagai hasilnya, tiga tes yang ditulis dan generator memungkinkan untuk memverifikasi dengan andal bahwa sejumlah elemen yang berbeda dapat ditambahkan ke repositori. Hanya tiga tes singkat! Itu pada dasarnya seluruh gagasan tes berbasis properti:

  • kami menemukan properti
  • memeriksa properti di tumpukan data yang berbeda
  • untung!

Omong-omong, pendekatan ini tidak bertentangan dengan prinsip-prinsip TDD - tes dapat ditulis dengan cara yang sama sebelum kode (setidaknya secara pribadi, saya biasanya melakukan ini). Hal lain adalah bahwa membuat tes menjadi hijau bisa jauh lebih sulit daripada tradisional, tetapi ketika akhirnya berhasil kita akan yakin bahwa kode benar-benar sesuai dengan bagian tertentu dari kontrak.

Ini semua baik dan bagus, tapi ...


Dengan semua daya tarik pendekatan pengujian berbasis properti, ada banyak masalah. Pada bagian ini saya akan mencoba membuat yang paling umum. Dan terlepas dari masalah dengan kompleksitas aktual dalam menemukan properti yang berguna (yang akan saya bahas kembali di bagian selanjutnya), menurut pendapat saya masalah terbesar bagi pemula sering kali adalah kepercayaan yang salah dalam cakupan yang baik. Memang, kami menulis beberapa tes yang menghasilkan ratusan kasus uji - apa yang bisa salah? Jika Anda melihat contoh dari bagian sebelumnya, sebenarnya ada banyak hal. Untuk mulai dengan, tes tertulis tidak memberikan jaminan bahwa storage.copy () akan benar-benar membuat salinan "dalam", dan bukan hanya menyalin pointer. Lubang lain - tidak ada verifikasi normal bahwa kunci dalam penyimpanan akan mengembalikan False jika kunci yang Anda cari tidak ada di toko. Dan daftarnya berlanjut. Nah, salah satu contoh favorit saya - katakanlah kita menulis semacam, dan untuk beberapa alasan kita berpikir bahwa satu tes yang memeriksa urutan elemen sudah cukup:

 input = arbitrary_list() output = sort(input) assert all(a <= b for a, b in zip(output, output[1:])) 

Dan implementasi seperti itu akan berlalu dengan sempurna

 def sort(input): return [1, 2, 3] 

Saya harap moral di sini jelas.

Masalah berikutnya, yang dalam arti dapat disebut konsekuensi dari dua sebelumnya, adalah bahwa menggunakan pengujian berbasis properti seringkali sangat sulit untuk mencapai cakupan yang benar-benar penuh. Tetapi menurut saya ini diselesaikan dengan sangat sederhana - Anda tidak perlu hanya menulis tes berdasarkan properti, tidak ada yang membatalkan tes tradisional. Selain itu, orang-orang begitu diatur sehingga jauh lebih mudah bagi mereka untuk memahami hal-hal dengan contoh nyata, yang juga mendukung penggunaan kedua pendekatan. Secara umum, saya mengembangkan sendiri kira-kira algoritma berikut - untuk menulis beberapa tes tradisional yang sangat sederhana, idealnya sehingga mereka dapat berfungsi sebagai contoh bagaimana API seharusnya digunakan. Segera setelah ada perasaan bahwa tes "untuk dokumentasi" sudah cukup, tetapi masih jauh dari cakupan lengkap - mulai menambahkan tes berdasarkan pada properti.

Sekarang untuk pertanyaan kerangka kerja, apa yang diharapkan dari mereka dan mengapa mereka diperlukan sama sekali - setelah semua, tidak ada yang melarang dengan tangan Anda untuk mengendarai tes dalam siklus, menyebabkan keacakan di dalam dan menikmati hidup. Bahkan, kegembiraan akan sampai tes pertama jatuh, dan itu baik jika secara lokal, dan tidak di beberapa CI. Pertama, karena pengujian berbasis properti dilakukan secara acak, Anda pasti membutuhkan cara untuk mereproduksi case yang terjatuh secara andal, dan kerangka kerja apa pun yang menghargai diri sendiri memungkinkan Anda melakukan ini. Pendekatan yang paling populer adalah dengan mengeluarkan seed tertentu ke konsol, yang dapat Anda buka secara manual di test runner dan andal memainkan case yang terjatuh (nyaman untuk debugging), atau membuat cache pada disk dengan sids "buruk", yang akan secara otomatis diperiksa terlebih dahulu ketika tes dimulai ( membantu pengulangan dalam CI). Aspek penting lainnya adalah minifikasi data (menyusut dalam sumber asing). Karena data dihasilkan secara acak, yaitu kesempatan yang sama sekali tidak palsu untuk mendapatkan test case yang jatuh dengan wadah 1000 elemen, yang masih merupakan "kesenangan" untuk debug. Oleh karena itu, kerangka kerja yang baik setelah menemukan kasus feylyaschy menerapkan sejumlah heuristik untuk mencoba menemukan set data input yang lebih kompak, yang tetap akan crash tes. Dan akhirnya - seringkali setengah dari fungsionalitas tes adalah generator data input, sehingga keberadaan generator dan primitif bawaan yang memungkinkan Anda untuk dengan cepat membangun yang lebih kompleks dari generator sederhana juga sangat membantu.

Ada juga kritik sesekali bahwa ada terlalu banyak tes logika berdasarkan properti. Namun, ini biasanya disertai dengan contoh gaya

 data = totally_arbitrary_data() perform_actions(sut, data) if is_category_a(data): assert property_a_holds(sut) else if is is_category_b(data): assert property_b_holds(sut) 

Sebenarnya, ini adalah antipattern yang umum (untuk pemula), jangan lakukan ini! Jauh lebih baik untuk membagi pengujian seperti itu menjadi dua yang berbeda, dan melewatkan data input yang tidak sesuai (dalam banyak kerangka kerja bahkan ada alat khusus untuk ini) jika peluang untuk mendapatkannya adalah kecil, atau menggunakan generator yang lebih khusus yang akan segera menghasilkan hanya data yang sesuai. Hasilnya harus seperti

 data = totally_arbitrary_data() assume(is_category_a(data)) perform_actions(sut, data) assert property_a_holds(sut) 

dan

 data = data_from_category_b() perform_actions(sut, data) assert property_b_holds(sut) 

Properti yang berguna, dan habitatnya


Oke, apa yang berguna untuk pengujian berdasarkan properti, tampaknya jelas, perangkap utama telah disortir ... meskipun tidak, hal utama masih belum jelas - dari mana properti ini berasal? Ayo coba cari.

Setidaknya jangan jatuh


Opsi termudah adalah memasukkan data acak ke dalam sistem yang sedang diuji dan memverifikasi bahwa itu tidak macet. Sebenarnya, ini adalah arah yang terpisah secara keseluruhan dengan nama modis fuzzing, di mana ada alat khusus (misalnya AFL alias American Fuzzy Lop), tetapi dengan beberapa peregangan dapat dianggap sebagai kasus pengujian khusus berdasarkan pada properti, dan jika sama sekali tidak ada ide dalam pikiran Jika tidak mendaki, maka Anda bisa memulainya. Namun demikian, sebagai aturan, secara eksplisit tes seperti itu jarang masuk akal, karena potensi penurunan biasanya keluar dengan sangat baik ketika memeriksa properti lainnya. Alasan utama mengapa saya menyebutkan "properti" ini adalah untuk mengarahkan pembaca ke fuzzers dan khususnya AFL (ada banyak artikel berbahasa Inggris tentang topik ini), yah, untuk melengkapi gambar.

Test oracle


Salah satu properti paling membosankan, tetapi sebenarnya hal yang sangat kuat yang dapat digunakan jauh lebih sering daripada yang terlihat. Idenya adalah bahwa kadang-kadang ada dua potong kode yang melakukan hal yang sama, tetapi dengan cara yang berbeda. Dan kemudian Anda khususnya tidak dapat memahami untuk menghasilkan data input yang berubah-ubah, mendorongnya ke dalam kedua opsi dan memverifikasi bahwa hasilnya cocok. Contoh aplikasi yang paling sering dikutip adalah ketika menulis versi fungsi yang dioptimalkan untuk meninggalkan opsi yang lambat namun sederhana dan menjalankan tes terhadapnya.

 input = arbitrary_list() assert quick_sort(input) == bubble_sort(input) 

Namun, penerapan properti ini tidak terbatas pada ini. Sebagai contoh, sangat sering ternyata fungsi yang diimplementasikan oleh sistem yang ingin kami uji adalah superset dari sesuatu yang sudah diterapkan, sering kali bahkan di perpustakaan bahasa standar. Secara khusus, biasanya sebagian besar fungsionalitas dari beberapa penyimpanan nilai kunci (dalam memori atau pada disk, berdasarkan pada pohon, tabel hash atau beberapa struktur data yang lebih eksotis seperti pohon merkle patricia) dapat diuji dengan kamus standar standar. Menguji segala macam CRUD - di sana juga.

Aplikasi lain yang menarik yang saya pribadi gunakan - kadang-kadang ketika menerapkan model numerik suatu sistem, beberapa kasus tertentu dapat dihitung secara analitis dan dibandingkan dengan mereka hasil simulasi. Dalam hal ini, sebagai suatu peraturan, jika Anda mencoba untuk memasukkan data yang sepenuhnya arbitrer ke dalam input, maka bahkan dengan implementasi yang benar, pengujian masih akan mulai turun karena keakuratan yang terbatas (dan, dengan demikian, penerapan) dari solusi numerik, tetapi selama proses perbaikan dengan memaksakan pembatasan pada data input yang dihasilkan, pembatasan yang sama ini menjadi dikenal.

Persyaratan dan Invarian


Gagasan utama di sini adalah bahwa seringkali persyaratan itu sendiri dirumuskan sehingga mudah digunakan sebagai properti. Dalam beberapa artikel tentang topik tersebut, invarian disorot secara terpisah, tetapi menurut saya perbatasan di sini terlalu limbung, karena sebagian besar invarian ini adalah konsekuensi langsung dari persyaratan, jadi saya mungkin akan membuang semuanya bersama-sama.

Daftar kecil contoh dari berbagai area yang cocok untuk memeriksa properti:

  • bidang kelas harus memiliki nilai yang sebelumnya ditugaskan (pengambil-setter)
  • repositori harus dapat membaca item yang direkam sebelumnya
  • menambahkan item yang sebelumnya tidak ada ke repositori tidak mempengaruhi item yang ditambahkan sebelumnya
  • dalam banyak kamus beberapa elemen berbeda dengan kunci yang sama tidak dapat disimpan
  • tinggi pohon seimbang seharusnya tidak lebih K cdotlog(N)dimana N- jumlah barang yang direkam
  • hasil pengurutan adalah daftar barang pesanan
  • hasil pengkodean base64 hanya boleh mengandung karakter base64
  • algoritma pembangunan rute harus mengembalikan urutan gerakan yang diizinkan yang akan mengarah dari titik A ke titik B
  • untuk semua titik isolat yang dibangun harus dipenuhi f(x,y)=const
  • algoritma verifikasi tanda tangan elektronik harus mengembalikan Benar jika tanda tangan itu nyata dan Salah sebaliknya
  • sebagai hasil dari ortonormalisasi, semua vektor pada dasarnya harus memiliki panjang satuan dan nol produk skalar timbal balik
  • transfer vektor dan operasi rotasi tidak boleh mengubah panjangnya

Pada prinsipnya, orang bisa mengatakan bahwa semuanya sudah lengkap, artikelnya sudah lengkap, menggunakan oracle tes atau mencari properti dalam persyaratan, tetapi ada beberapa "kasus khusus" yang lebih menarik yang ingin saya tunjukkan secara terpisah.

Pengujian induksi dan keadaan


Terkadang Anda perlu menguji sesuatu dengan status. Dalam hal ini, cara termudah:

  • tulis tes yang memeriksa kebenaran keadaan awal (misalnya, bahwa wadah yang baru saja dibuat kosong)
  • tulis sebuah generator yang menggunakan serangkaian operasi acak akan membawa sistem ke keadaan arbitrer
  • tulis tes untuk semua operasi menggunakan hasil generator sebagai keadaan awal

Sangat mirip dengan induksi matematika:

  • buktikan pernyataan 1
  • buktikan pernyataan N +1, dengan anggapan bahwa pernyataan N itu benar

Metode lain (kadang-kadang memberikan sedikit informasi lebih lanjut tentang di mana ia rusak) adalah untuk menghasilkan urutan peristiwa yang dapat diterima, menerapkannya ke sistem yang sedang diuji dan memeriksa properti setelah setiap langkah.

Bolak-balik


Jika tiba-tiba ada kebutuhan untuk menguji beberapa fungsi untuk konversi langsung dan membalikkan beberapa data, maka pertimbangkan bahwa Anda sangat beruntung:

 input = arbitrary_data() assert decode(encode(input)) == input 

Bagus untuk pengujian:

  • serialisasi-deserialization
  • dekripsi enkripsi
  • encoding-decoding
  • mengubah matriks dasar menjadi angka empat dan sebaliknya
  • transformasi koordinat langsung dan terbalik
  • mengarahkan dan membalikkan transformasi fourier

Kasus khusus, tetapi menarik adalah inversinya:

 input = arbitrary_data() assert invert(invert(input)) == input 

Contoh yang mencolok adalah inversi atau transposisi dari suatu matriks.

Idempotensi


Beberapa operasi tidak mengubah hasil penggunaan berulang. Contoh umum:

  • menyortir
  • normalisasi vektor dan basis
  • menambahkan kembali item yang ada ke set atau kamus
  • merekam ulang data yang sama di beberapa properti objek
  • casting data ke bentuk kanonik (spasi di JSON mengarah ke gaya terpadu misalnya)

Idempotency juga dapat digunakan untuk menguji serialisasi-deserialisasi jika dekode biasa (penyandian (input)) == metode input tidak cocok karena kemungkinan representasi berbeda untuk data input setara (sekali lagi, ruang ekstra di beberapa JSON):

 def normalize(input): return decode(encode(input)) input = arbitrary_data() assert normalize(normalize(input)) == normalize(input) 

Berbagai cara, satu hasil


Di sini muncul ide untuk mengeksploitasi fakta bahwa kadang-kadang ada beberapa cara untuk melakukan hal yang sama. Ini mungkin tampak seperti kasus khusus dari oracle tes, tetapi dalam kenyataannya tidak begitu. Contoh paling sederhana adalah menggunakan komutatifitas dari beberapa operasi:

 a = arbitrary_value() b = arbitrary_value() assert a + b == b + a 

Ini mungkin tampak sepele, tetapi ini adalah cara yang bagus untuk menguji:

  • penambahan dan penggandaan angka dalam representasi non-standar (bigint, rasional, itu saja)
  • "Penambahan" titik pada kurva elips di bidang terbatas (halo, kriptografi!)
  • gabungan set (yang di dalamnya dapat memiliki struktur data yang benar-benar non-sepele)

Selain itu, penambahan elemen ke kamus memiliki properti yang sama:

 A = dict() A[key_a] = value_a A[key_b] = value_b B = dict() B[key_b] = value_b B[key_a] = value_a assert A == B 

Opsi ini lebih rumit - untuk waktu yang lama saya berpikir tentang bagaimana menggambarkannya dalam kata-kata, tetapi hanya notasi matematika yang muncul di pikiran. Secara umum, transformasi seperti itu biasa terjadi f(x)yang dimiliki oleh properti f(x+y)=f(x) cdotf(y), dan argumen serta hasil fungsi tidak harus hanya berupa angka, tetapi operasi +dan  cdot- hanya beberapa operasi biner pada objek-objek ini. Apa yang dapat Anda uji dengan ini:

  • penambahan dan penggandaan semua jenis angka aneh, vektor, matriks, angka empat ( a cdot(x+y)=a cdotx+a cdoty)
  • operator linier, khususnya semua jenis integral, diferensial, konvolusi, filter digital, transformasi Fourier, dll. ( F[x+y]=F[x]+F[y])
  • operasi pada objek yang identik dalam representasi yang berbeda, misalnya

    • M(qa cdotqb)=M(qa) cdotM(qb)dimana qadan qbAdalah angka empat tunggal, dan M(q)- operasi mengubah angka empat menjadi matriks basis setara
    • F[a circb]=F[a] cdotF[b]dimana adan bApakah sinyal  circ- belok  cdot- perkalian, dan F- Transformasi Fourier


Contoh tugas yang sedikit lebih β€œbiasa” - untuk menguji beberapa algoritma penggabungan kamus yang rumit, Anda dapat melakukan sesuatu seperti ini:

 a = arbitrary_list_of_kv_pairs() b = arbitrary_list_of_kv_pairs() result = as_dict(a) result.merge(as_dict(b)) assert result == as_dict(a + b) 

Alih-alih sebuah kesimpulan


Itu pada dasarnya semua yang ingin saya ceritakan di artikel ini. Saya harap ini menarik, dan sedikit lebih banyak orang akan mulai mempraktikkan semua ini. Untuk membuat tugas sedikit lebih mudah, saya akan memberi Anda daftar kerangka kerja berbagai tingkat validitas untuk berbagai bahasa:


Dan, tentu saja, terima kasih khusus kepada orang-orang yang pernah menulis artikel bagus, berkat yang saya pelajari tentang pendekatan ini beberapa tahun yang lalu, berhenti khawatir dan mulai menulis tes berdasarkan pada properti:

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


All Articles