Pengembangan program berkualitas tinggi menyiratkan bahwa program dan bagian-bagiannya diuji. Pengujian unit klasik melibatkan memecah program besar menjadi blok-blok kecil yang nyaman untuk pengujian. Atau, jika pengembangan tes berlangsung secara paralel dengan pengembangan kode atau tes dikembangkan sebelum program, maka program tersebut awalnya dikembangkan dalam blok kecil yang cocok untuk persyaratan tes.
Salah satu jenis pengujian unit dapat dianggap pengujian berbasis layak (pendekatan ini diterapkan, misalnya, di QuickCheck , perpustakaan ScalaCheck ). Pendekatan ini didasarkan pada penemuan properti universal yang harus valid untuk input data apa pun. Misalnya, serialisasi diikuti oleh deserialisasi harus menghasilkan objek yang sama . Atau, pengurutan ulang tidak boleh mengubah urutan item dalam daftar . Untuk memverifikasi properti universal seperti itu, perpustakaan di atas mendukung mekanisme untuk menghasilkan data input acak. Pendekatan ini bekerja sangat baik untuk program yang didasarkan pada hukum matematika yang berfungsi sebagai properti universal yang valid untuk kelas program yang luas. Bahkan ada perpustakaan properti matematika yang sudah jadi - disiplin - yang memungkinkan Anda untuk memeriksa kinerja properti ini dalam program baru (contoh yang baik dari tes menggunakan kembali).
Kadang-kadang ternyata perlu untuk menguji program yang kompleks tanpa dapat menguraikannya menjadi bagian-bagian yang dapat diverifikasi secara independen. Dalam hal ini, program pengujian adalah hitam white box (putih - karena kami memiliki kesempatan untuk mempelajari struktur internal program).
Di bawah cut, beberapa pendekatan untuk menguji program yang kompleks dengan satu input dengan berbagai tingkat kompleksitas (keterlibatan) dan berbagai tingkat cakupan dijelaskan.
* Dalam artikel ini, kami menganggap bahwa program yang sedang diuji dapat direpresentasikan sebagai fungsi murni tanpa kondisi internal. (Beberapa pertimbangan di bawah ini dapat diterapkan jika keadaan internal ada, tetapi dimungkinkan untuk mengatur ulang keadaan ini ke nilai tetap.)
Bangku tes
Pertama-tama, karena hanya satu fungsi yang diuji, kode panggilan yang selalu sama, kita tidak perlu membuat unit test terpisah. Semua tes semacam itu akan sama, akurat untuk input dan pemeriksaan. Cukup cukup untuk mengirimkan data sumber ( input
) dalam satu lingkaran dan memeriksa hasilnya ( expectedOutput
). Untuk mengidentifikasi set masalah dari data pengujian dalam hal deteksi kesalahan, semua data pengujian harus diberi label. Dengan demikian, satu set data uji dapat direpresentasikan sebagai tiga:
case class TestCase[A, B](label: String, input: A, expectedOutput: B)
Hasil dari satu proses dapat direpresentasikan sebagai TestCaseResult
:
case class TestCaseResult[A, B](testCase: TestCase[A, B], actualOutput: Try[B])
(Kami menyajikan hasil peluncuran menggunakan Try
untuk menangkap kemungkinan pengecualian.)
Untuk menyederhanakan menjalankan semua data uji melalui program yang sedang diuji, Anda dapat menggunakan fungsi pembantu yang akan memanggil program untuk setiap nilai input:
def runTestCases[A, B](cases: Seq[TestCase[A, B])(f: A => B): Seq[TestCaseResult[A, B]] = cases .map{ testCase => TestCaseResult(testCase, Try{ f(testCase.input) } ) } .filter(r => r.actualOutput != Success(r.testCase.expectedOutput))
Fungsi pembantu ini akan mengembalikan data dan hasil yang bermasalah yang berbeda dari yang diharapkan.
Untuk kenyamanan, Anda dapat memformat hasil tes.
def report(results: Seq[TestCaseResult[_, _]]): String = s"Failed ${results.length}:\n" + results .map(r => r.testCase.label + ": expected " + r.testCase.expectedOutput + ", but got " + r.actualOutput) .mkString("\n")
dan tampilkan laporan hanya jika ada kesalahan:
val testCases = Seq( TestCase("1", 0, 0) ) test("all test cases"){ val testBench = runTestCases(testCases) _ val results = testBench(f) assert(results.isEmpty, report(results)) }
Persiapan input
Dalam kasus yang paling sederhana, Anda dapat secara manual membuat data uji untuk menguji program, menuliskannya langsung dalam kode uji, dan menggunakannya, seperti yang ditunjukkan di atas. Sering kali ternyata bahwa kasus-kasus data uji yang menarik memiliki banyak kesamaan dan dapat disajikan sebagai beberapa contoh dasar, dengan perubahan kecil.
val baseline = MyObject(...)
Saat bekerja dengan struktur data yang tidak dapat diubah yang bersarang, lensa sangat membantu, misalnya, dari perpustakaan Monocle :
val baseline = ??? val testObject1 = (field1 composeLens field2).set("123")(baseline)
Lensa memungkinkan Anda untuk "secara elegan" memodifikasi bagian struktur data yang bersarang: Setiap lensa adalah pengambil dan penyetel untuk satu properti. Lensa dapat digabungkan untuk menghasilkan lensa yang "fokus" pada level selanjutnya.
Menggunakan DSL untuk Menampilkan Perubahan
Selanjutnya, kami akan mempertimbangkan pembentukan data uji dengan membuat perubahan pada beberapa objek input awal. Biasanya, untuk mendapatkan objek tes yang kita butuhkan, kita perlu membuat beberapa perubahan. Dalam hal ini, sangat berguna untuk memasukkan daftar perubahan dalam deskripsi teks dari TestCase:
val testCases = Seq( TestCase("baseline", baseline, ???), TestCase("baseline + " + "(field1 = 123) + " +
Maka kita akan selalu tahu untuk data uji apa pengujian dilakukan.
Agar daftar perubahan tekstual tidak menyimpang dari perubahan yang sebenarnya, Anda harus mengikuti prinsip "satu versi kebenaran." (Jika informasi yang sama diperlukan / digunakan di beberapa titik, maka harus ada satu sumber utama informasi unik, dan informasi harus didistribusikan ke semua titik penggunaan lainnya secara otomatis, dengan transformasi yang diperlukan. Jika prinsip ini dilanggar dan penyalinan informasi secara manual tidak dapat dihindari . informasi versi perbedaan di berbagai titik dalam kata lain dalam deskripsi data uji, kita melihat satu, dan data uji -. contoh lain, menyalin perubahan field2 = "456"
dan menyesuaikan dalam field3 = "789"
kita Mauger sengaja lupa untuk memperbaiki deskripsi. Akibatnya, deskripsi hanya akan mencerminkan dua perubahan dari tiga.)
Dalam kasus kami, sumber utama informasi adalah perubahan itu sendiri, atau lebih tepatnya, kode sumber program yang membuat perubahan. Kami ingin menyimpulkan dari mereka teks yang menjelaskan perubahan. Begitu saja, sebagai opsi pertama, Anda dapat menyarankan menggunakan makro yang akan menangkap kode sumber perubahan, dan menggunakan kode sumber sebagai dokumentasi. Tampaknya, ini adalah cara yang baik dan relatif tidak rumit untuk mendokumentasikan perubahan aktual dan mungkin juga diterapkan dalam beberapa kasus. Sayangnya, jika kami menyajikan perubahan dalam teks biasa, kami kehilangan kemampuan untuk membuat transformasi yang berarti dari daftar perubahan. Misalnya, mendeteksi dan menghilangkan perubahan duplikat atau tumpang tindih, buatlah daftar perubahan dengan cara yang nyaman bagi pengguna akhir.
Untuk dapat menangani perubahan, Anda harus memiliki model terstruktur darinya. Model tersebut harus cukup ekspresif untuk menggambarkan semua perubahan yang menarik minat kita. Bagian dari model ini, misalnya, akan menjadi pengalamatan bidang objek, konstanta, operasi penugasan.
Model perubahan harus memungkinkan untuk menyelesaikan tugas-tugas berikut:
- Memunculkan contoh model perubahan. (Yaitu, sebenarnya membuat daftar perubahan tertentu.)
- Pembentukan deskripsi tekstual dari perubahan.
- Menerapkan perubahan pada objek domain.
- Melakukan transformasi optimasi pada model.
Jika bahasa pemrograman universal digunakan untuk membuat perubahan, maka mungkin sulit untuk mewakili perubahan ini dalam model. Kode sumber program dapat menggunakan konstruksi kompleks yang tidak didukung oleh model. Program semacam itu dapat menggunakan pola-pola sekunder, seperti lensa atau metode copy
, untuk mengubah bidang suatu objek, yang merupakan abstraksi tingkat rendah relatif terhadap level model perubahan. Akibatnya, analisis tambahan dari pola tersebut mungkin diperlukan untuk menampilkan contoh perubahan. Jadi, awalnya pilihan yang baik menggunakan makro tidak terlalu nyaman.
Cara lain untuk membuat instance dari model perubahan bisa menjadi bahasa khusus (DSL), yang membuat objek model perubahan menggunakan serangkaian metode ekstensi dan operator tambahan. Nah, dalam kasus yang paling sederhana, contoh dari model perubahan dapat dibuat langsung melalui konstruktor.
Ubah Detail BahasaBahasa ganti adalah konstruksi yang agak rumit yang mencakup beberapa komponen, yang, pada gilirannya, nontrivial.
- Model struktur data.
- Ubah Model.
- Sebenarnya Embedded (?) DSL - konstruksi tambahan, metode ekstensi, untuk konstruksi perubahan yang nyaman.
- Penerjemah perubahan yang memungkinkan Anda untuk benar-benar "memodifikasi" suatu objek (sebenarnya, tentu saja, membuat salinan yang dimodifikasi).
Berikut adalah contoh program yang ditulis menggunakan DSL:
val target: Entity[Target]
Yaitu, menggunakan metode ekstensi \
dan :=
, PropertyAccess
, objek SetProperty
dibentuk dari target
dibuat sebelumnya, field1
, subobject
, field2
. Selain itu, karena konversi tersirat (berbahaya), string "123" dimasukkan ke LiftedString
(Anda dapat melakukannya tanpa konversi implisit dan memanggil metode yang terkait secara eksplisit: lift("123")
).
Ontologi yang diketik dapat digunakan sebagai model data (lihat https://habr.com/post/229035/ dan https://habr.com/post/222553/ ). (Singkatnya: nama objek dideklarasikan yang mewakili properti dari semua jenis domain: val field1: Property[Target, String]
.) Dalam kasus ini, data aktual dapat disimpan, misalnya, dalam bentuk JSON. Kenyamanan ontologi yang diketik dalam kasus kami terletak pada kenyataan bahwa model perubahan biasanya beroperasi dengan properti individual dari objek, dan ontologi hanya menyediakan alat yang sesuai untuk mengatasi properti.
Untuk mewakili perubahan, Anda memerlukan seperangkat kelas dari paket yang sama dengan kelas SetProperty
atas:
Modify
- aplikasi fungsi,Changes
- menerapkan beberapa perubahan secara berurutanForEach
- terapkan perubahan pada setiap item dalam koleksi,- dll.
Penerjemah bahasa perubahan adalah evaluator ekspresi rekursif reguler berdasarkan PatternMatching. Sesuatu seperti:
def eval(expression: DslExpression, gamma: Map[String, Any]): Any = expression match { case LiftedString(str) => str case PropertyAccess(obj, prop) => Getter(prop)(gamma).get(obj) } def change[T] (expression: DslChangeExpression, gamma: Map[String, Any], target: T): T = expression match { case SetProperty(path, valueExpr) => val value = eval(valueExpr, gamma) Setter(path)(gamma).set(value)(target) }
Untuk langsung beroperasi pada properti objek, Anda harus menentukan pengambil dan penyetel untuk setiap properti yang digunakan dalam model perubahan. Ini dapat dicapai dengan mengisi peta antara sifat ontologis dan lensa yang sesuai.
Pendekatan ini secara keseluruhan bekerja, dan memang memungkinkan Anda untuk menggambarkan perubahan sekali, tetapi secara bertahap ada kebutuhan untuk mewakili perubahan yang semakin kompleks dan model perubahan semakin berkembang. Misalnya, jika Anda perlu mengubah properti menggunakan nilai properti lain dari objek yang sama (misalnya, field1 = field2 + 1
), maka Anda perlu mendukung variabel di tingkat DSL. Dan jika mengubah properti adalah nontrivial, maka pada tingkat DSL, dukungan untuk ekspresi dan fungsi aritmatika diperlukan.
Pengujian cabang
Kode uji bisa linear, dan kemudian, pada umumnya, satu set data uji cukup untuk memahami apakah itu berfungsi. Jika ada cabang ( if-then-else
), Anda harus menjalankan kotak putih setidaknya dua kali dengan data input yang berbeda sehingga kedua cabang dieksekusi. Jumlah set input data yang cukup untuk mencakup semua cabang tampaknya secara numerik sama dengan kompleksitas cyclomatic dari kode dengan cabang.
Bagaimana cara membentuk semua set data input? Karena kita berhadapan dengan kotak putih, kita dapat mengisolasi kondisi percabangan dan memodifikasi objek input dua kali sehingga dalam satu kasus satu cabang dieksekusi, dalam kasus lain yang lain. Pertimbangkan sebuah contoh:
if (object.field1 == "123") A else B
Memiliki kondisi seperti itu, kami dapat membentuk dua kasus uji:
val testCase1 = TestCase("A", field1.set("123")(baseline), ) val testCase2 = TestCase("B", field1.set("123" + "1">)(baseline), )
(Jika salah satu skenario pengujian tidak dapat dibuat, maka kita dapat mengasumsikan bahwa kode mati telah terdeteksi, dan kondisi, bersama dengan cabang yang sesuai, dapat dihapus dengan aman.)
Jika properti independen suatu objek diperiksa di beberapa cabang, maka cukup mudah untuk membentuk set lengkap objek pengujian yang dimodifikasi yang sepenuhnya mencakup semua kombinasi yang mungkin.
DSL untuk membentuk semua kombinasi perubahanMari kita pertimbangkan secara lebih terperinci mekanisme yang memungkinkan untuk membentuk semua daftar kemungkinan perubahan yang menyediakan cakupan penuh dari semua cabang. Untuk menggunakan daftar perubahan selama pengujian, kita perlu menggabungkan semua perubahan menjadi satu objek, yang akan kita serahkan ke input dari kode yang diuji, yaitu, dukungan untuk komposisi diperlukan. Untuk melakukan ini, Anda bisa menggunakan DSL di atas untuk memodelkan perubahan, dan kemudian daftar perubahan sederhana sudah cukup, atau Anda dapat menyajikan satu perubahan sebagai fungsi modifikasi T => T
:
val change1: T => T = field1.set("123")(_)
maka rantai perubahan hanya akan menjadi komposisi fungsi:
val changes = change1 compose change2
atau, untuk daftar perubahan:
val rawChangesList: Seq[T => T] = Seq(change1, change2) val allChanges: T => T = rawChangesList.foldLeft(identity)(_ compose _)
Untuk secara kompak merekam semua perubahan yang terkait dengan semua cabang yang memungkinkan, Anda dapat menggunakan DSL dengan level abstraksi berikut, yang mensimulasikan struktur kotak putih yang diuji:
val tests: Seq[(String, T => T)] = IF("field1 == '123'")
Di sini koleksi tests
berisi perubahan agregat yang sesuai dengan semua kemungkinan kombinasi cabang. Parameter tipe String
akan berisi semua nama kondisi dan semua deskripsi dari perubahan dari mana fungsi perubahan agregat terbentuk. Dan elemen kedua dari sepasang tipe T => T
hanyalah fungsi agregat dari perubahan yang diperoleh sebagai hasil dari komposisi perubahan individu.
Untuk mendapatkan objek yang diubah, Anda perlu menerapkan semua fungsi perubahan agregat ke objek dasar:
val tests2: Seq[(String, T)] = tests.map(_.map_2(_(baseline)))
Sebagai hasilnya, kami mendapatkan koleksi pasangan, dan garis akan menjelaskan perubahan yang diterapkan, dan elemen kedua dari pasangan akan menjadi objek di mana semua perubahan ini digabungkan.
Berdasarkan pada struktur model kode yang diuji dalam bentuk pohon, daftar perubahan akan mewakili jalur dari root ke lembaran pohon ini. Dengan demikian, sebagian besar perubahan akan digandakan. Anda dapat menyingkirkan duplikasi ini menggunakan opsi DSL, di mana perubahan diterapkan langsung ke objek dasar saat Anda bergerak di sepanjang cabang. Dalam hal ini, lebih sedikit perhitungan yang tidak perlu akan dilakukan.
Karena kita berurusan dengan kotak putih, kita bisa melihat semua cabang. Ini memungkinkan untuk membangun model logika yang terkandung dalam kotak putih dan menggunakan model untuk menghasilkan data uji. Jika kode tes ditulis dalam Scala, Anda dapat, misalnya, menggunakan scalameta untuk membaca kode, dengan konversi selanjutnya ke model logika. Sekali lagi, seperti dalam masalah pemodelan logika perubahan yang dibahas sebelumnya, sulit bagi kita untuk memodelkan semua kemungkinan bahasa universal. Lebih lanjut, kami akan menganggap bahwa kode yang diuji diimplementasikan menggunakan subset terbatas dari bahasa, atau dalam bahasa lain atau DSL, yang pada awalnya terbatas. Ini memungkinkan kami untuk fokus pada aspek-aspek bahasa yang menarik bagi kami.
Pertimbangkan contoh kode yang berisi satu cabang:
if(object.field1 == "123") A else B
Kondisi ini membagi set nilai field1
menjadi dua kelas kesetaraan: == "123"
dan != "123"
. Dengan demikian, seluruh rangkaian data input juga dibagi menjadi dua kelas kesetaraan sehubungan dengan kondisi ini - ClassCondition1IsTrue
dan ClassCondition1IsFalse
. Dari sudut pandang kelengkapan cakupan, cukup bagi kita untuk mengambil setidaknya satu contoh dari dua kelas ini untuk mencakup cabang A
dan B
Untuk kelas pertama, kita bisa membuat contoh, dalam arti tertentu, dengan cara yang unik: ambil objek acak, tetapi ubah field1
ke "123"
. Selain itu, objek pasti akan ClassCondition1IsTrue
di kelas ekivalensi ClassCondition1IsTrue
dan perhitungan akan pergi bersama cabang A
Ada lebih banyak contoh untuk kelas kedua. Salah satu cara untuk menghasilkan beberapa contoh dari kelas kedua adalah dengan menghasilkan objek input sewenang-wenang dan membuang yang dengan field1 == "123"
. Cara lain: untuk mengambil objek acak, tetapi ubah field1
ke "123" + "*"
(untuk modifikasi, Anda dapat menggunakan perubahan apa pun di baris kontrol untuk memastikan bahwa baris baru tidak sama dengan garis kontrol).
Arbitrary
- Arbitrary
dan Gen
dari perpustakaan ScalaCheck sangat cocok sebagai Arbitrary
data acak.
Pada dasarnya, kita memanggil fungsi boolean yang digunakan dalam if
. Yaitu, kami menemukan semua nilai objek input yang fungsi Boolean ini dievaluasi menjadi true
- ClassCondition1IsTrue
, dan semua nilai objek input yang diterima false
- ClassCondition1IsFalse
.
Dengan cara yang serupa, dimungkinkan untuk menghasilkan data yang cocok untuk kendala yang dihasilkan oleh operator kondisional sederhana dengan konstanta (lebih / kurang dari konstanta, termasuk dalam himpunan, dimulai dengan konstanta). Kondisi seperti itu mudah dibalik. Bahkan jika fungsi-fungsi sederhana dipanggil dalam kode uji, kita dapat mengganti panggilan mereka dengan definisi mereka (sebaris) dan masih membalikkan ekspresi kondisional.
Fungsi yang sulit dibalik
Situasi berbeda ketika kondisi menggunakan fungsi yang sulit untuk dibalik. Misalnya, jika fungsi hash digunakan, maka sepertinya tidak mungkin untuk secara otomatis menghasilkan contoh yang memberikan nilai yang diinginkan dari kode hash.
Dalam hal ini, Anda dapat menambahkan parameter tambahan ke objek input yang mewakili hasil perhitungan fungsi, mengganti panggilan fungsi dengan panggilan ke parameter ini, dan memperbarui parameter ini, meskipun ada pelanggaran koneksi fungsional:
if(sha(object.field1)=="a9403...") ... // ==> if(object.sha_field1 == "a9403...") ...
Parameter tambahan memungkinkan untuk eksekusi kode di dalam cabang, tetapi, jelas, itu dapat menyebabkan hasil yang sebenarnya salah. Artinya, program uji akan menghasilkan hasil yang tidak pernah bisa diamati dalam kenyataan. Namun demikian, memeriksa bagian dari kode yang tidak dapat diakses oleh kami masih bermanfaat dan dapat dianggap sebagai bentuk pengujian unit. Bagaimanapun, bahkan selama pengujian unit, subfungsi dipanggil dengan argumen yang mungkin tidak pernah digunakan dalam program.
Dengan manipulasi seperti itu, kami mengganti (mengganti) objek uji. Namun, dalam arti tertentu, program yang baru dibangun ini termasuk logika dari program lama. Memang, jika sebagai nilai dari parameter buatan baru kita mengambil hasil penghitungan fungsi yang kita ganti dengan parameter, program akan menghasilkan hasil yang sama. Tampaknya menguji program yang dimodifikasi mungkin masih menarik. Anda hanya perlu mengingat dalam kondisi apa program yang diubah akan berperilaku sama dengan yang asli.
Kondisi tergantung
, . , , , . , . (, , x > 0
, — x <= 1
. , — (-∞, 0]
, (0, 1]
, (1, +∞)
, — .)
, , , true
false
. , , " " .
, , :
if(x > 0) if(y > 0) if (y > x)
( > 0
, — y > x
.)
"", , , , , . , " " .
, "", ( y == x + 1
), , .
"" ( y > x + 1 && y < x + 2
), , .
, , - , "c " ( Symbolic Execution , ), . ( field1 = field1_initial_value
). , . :
val a = field1 + 10
— true
false
. . . Sebagai contoh
if(a > 0) A else B
, , , , . , (, , ).
. , , . , . , .
, . . , , . , , , . , , , , , ?
Y- ( " " , stackoverflow:What is a Y-combinator? (2- ) , habr: Y- 7 ). , . ( , , .) . , . , "" . Y- " " ( ).
( ). , . , . , . , , , TestCase
'. , , ( throw
Nothing
bottom
, ). .
, . . , , . , . , , . , , , , . , . , .
, , , , 100% . , , . Hm . , , , ? , , - .
:
- .
- ( ).
- , .
- , .
, , . -, , , , . -, , , ( , ), , , "" . / , .
Kesimpulan
" " " ". , , , , . , .
, , , , . -, , ( ), . -, -, . DSL, , . -, , . -, , ( , , ). .
, , . , , - .
Ucapan Terima Kasih
@mneychev .