Kapan saya harus mengejek?

138

Saya memiliki pemahaman dasar mengenai benda tiruan dan benda palsu, tetapi saya tidak yakin apakah saya memiliki perasaan tentang kapan / di mana menggunakan ejekan - terutama karena akan berlaku untuk skenario ini di sini .

Esteban Araya
sumber
Saya sarankan hanya mengejek dependensi out-of-proses dan hanya mereka, interaksi dengan yang diamati secara eksternal (server SMTP, bus pesan, dll). Jangan mengejek basis data, ini adalah detail implementasi. Lebih lanjut tentang hal ini di sini: enterprisecraftsmanship.com/posts/when-to-mock
Vladimir

Jawaban:

122

Tes unit harus menguji codepath tunggal melalui metode tunggal. Ketika eksekusi suatu metode melewati di luar metode itu, ke objek lain, dan kembali lagi, Anda memiliki ketergantungan.

Ketika Anda menguji jalur kode dengan ketergantungan yang sebenarnya, Anda bukan pengujian unit; Anda sedang menguji integrasi. Meskipun bagus dan perlu, itu bukan pengujian unit.

Jika ketergantungan Anda buggy, tes Anda dapat dipengaruhi sedemikian rupa untuk mengembalikan false positive. Misalnya, Anda dapat meneruskan dependensi ke null yang tidak terduga, dan dependensi mungkin tidak menghasilkan null seperti yang didokumentasikan harus dilakukan. Tes Anda tidak menemukan pengecualian argumen nol sebagaimana mestinya, dan tes berlalu.

Juga, Anda mungkin menemukan kesulitan, jika bukan tidak mungkin, untuk secara andal mendapatkan objek dependen untuk mengembalikan apa yang Anda inginkan selama tes. Itu juga termasuk melempar pengecualian yang diharapkan dalam tes.

Mock menggantikan ketergantungan itu. Anda menetapkan harapan pada panggilan ke objek dependen, menetapkan nilai pengembalian tepat yang harus diberikan untuk melakukan tes yang Anda inginkan, dan / atau pengecualian apa yang dilemparkan sehingga Anda dapat menguji kode penanganan pengecualian Anda. Dengan cara ini Anda dapat menguji unit yang dimaksud dengan mudah.

TL; DR: mengejek setiap ketergantungan yang disentuh tes unit Anda.

Drew Stephens
sumber
164
Jawaban ini terlalu radikal. Tes unit dapat dan harus dilakukan lebih dari satu metode tunggal, asalkan semuanya milik unit kohesif yang sama. Melakukan yang sebaliknya akan membutuhkan terlalu banyak cemoohan / tipuan, yang mengarah pada tes yang rumit dan rapuh. Hanya dependensi yang tidak benar-benar milik unit yang diuji yang harus diganti melalui ejekan.
Rogério
10
Jawaban ini juga terlalu optimis. Akan lebih baik jika dimasukkan kekurangan @ Jan dari objek tiruan.
Jeff Axelrod
1
Bukankah ini lebih merupakan argumen untuk menyuntikkan dependensi untuk tes daripada mengejek secara khusus? Anda bisa mengganti "mock" dengan "stub" di jawaban Anda. Saya setuju bahwa Anda harus mengejek atau mematikan dependensi yang signifikan. Saya telah melihat banyak kode tiruan-berat yang pada dasarnya berakhir menerapkan kembali bagian-bagian dari objek tiruan; ejekan tentu bukan peluru perak.
Draemon
2
Mock setiap ketergantungan unit tes Anda menyentuh. Ini menjelaskan semuanya.
Teoman shipahi
2
TL; DR: mengejek setiap ketergantungan yang disentuh tes unit Anda. - ini bukan pendekatan yang bagus, kata mockito sendiri - jangan mengejek segalanya. (downvoted)
p_champ
167

Objek tiruan berguna ketika Anda ingin menguji interaksi antara kelas yang diuji dan antarmuka tertentu.

Sebagai contoh, kami ingin menguji bahwa sendInvitations(MailServer mailServer)panggilan metode MailServer.createMessage()tepat sekali, dan juga panggilan MailServer.sendMessage(m)tepat sekali, dan tidak ada metode lain yang dipanggil pada MailServerantarmuka. Inilah saatnya kita bisa menggunakan benda tiruan.

Dengan objek tiruan, alih-alih lulus nyata MailServerImpl, atau tes TestMailServer, kita bisa melewati implementasi tiruan dari MailServerantarmuka. Sebelum kita melewati tiruan MailServer, kita "melatih" itu, sehingga ia tahu metode apa yang dibutuhkan untuk diharapkan dan nilai pengembalian apa yang akan dikembalikan. Pada akhirnya, objek tiruan menegaskan, bahwa semua metode yang diharapkan disebut seperti yang diharapkan.

Ini kedengarannya bagus secara teori, tetapi ada juga beberapa kerugian.

Kekurangan tiruan

Jika Anda memiliki kerangka kerja tiruan di tempat, Anda tergoda untuk menggunakan objek tiruan setiap kali Anda harus melewati antarmuka ke kelas yang sedang diuji. Dengan cara ini Anda akhirnya menguji interaksi bahkan ketika itu tidak perlu . Sayangnya, pengujian interaksi yang tidak diinginkan (tidak disengaja) buruk, karena Anda menguji bahwa persyaratan tertentu diimplementasikan dengan cara tertentu, alih-alih implementasi menghasilkan hasil yang diperlukan.

Berikut ini contoh dalam pseudocode. Misalkan kita telah membuat MySorterkelas dan kami ingin mengujinya:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

(Dalam contoh ini kami mengasumsikan bahwa itu bukan algoritma penyortiran tertentu, seperti penyortiran cepat, yang ingin kami uji; dalam hal ini, tes yang terakhir sebenarnya akan valid.)

Dalam contoh ekstrem seperti itu, jelas mengapa contoh terakhir ini salah. Ketika kami mengubah implementasi MySorter, tes pertama melakukan pekerjaan yang baik untuk memastikan kami masih mengurutkan dengan benar, yang merupakan inti dari tes - mereka memungkinkan kami untuk mengubah kode dengan aman. Di sisi lain, tes terakhir selalu rusak dan secara aktif berbahaya; itu menghambat refactoring.

Mengolok-olok sebagai bertopik

Kerangka kerja tiruan sering memungkinkan penggunaan yang tidak terlalu ketat, di mana kita tidak harus menentukan dengan tepat berapa kali metode harus dipanggil dan parameter apa yang diharapkan; mereka memungkinkan membuat benda tiruan yang digunakan sebagai bertopik .

Misalkan kita memiliki metode sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)yang ingin kita uji. The PdfFormatterobjek dapat digunakan untuk membuat undangan. Inilah tesnya:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

Dalam contoh ini, kami tidak terlalu peduli tentang PdfFormatterobjek, jadi kami hanya melatihnya untuk menerima panggilan apa pun secara diam-diam dan mengembalikan beberapa nilai pengembalian kalengan yang masuk akal untuk semua metode yang sendInvitation()memanggil pada saat ini. Bagaimana cara kami membuat daftar metode pelatihan ini? Kami hanya menjalankan tes dan terus menambahkan metode sampai tes berlalu. Perhatikan, bahwa kami melatih rintisan untuk merespons suatu metode tanpa memiliki petunjuk mengapa perlu menyebutnya, kami hanya menambahkan semua yang dikeluhkan oleh tes. Kami senang, tes lulus.

Tetapi apa yang terjadi kemudian, ketika kita mengubah sendInvitations(), atau beberapa kelas lain yang sendInvitations()menggunakan, untuk membuat lebih banyak pdf mewah? Tes kami tiba-tiba gagal karena sekarang lebih banyak metode PdfFormatterdipanggil dan kami tidak melatih rintisan kami untuk mengharapkannya. Dan biasanya bukan hanya satu tes yang gagal dalam situasi seperti ini, ini adalah tes apa pun yang menggunakan, secara langsung atau tidak langsung, sendInvitations()metode tersebut. Kami harus memperbaiki semua tes itu dengan menambahkan lebih banyak pelatihan. Perhatikan juga, bahwa kita tidak dapat menghapus metode yang tidak lagi diperlukan, karena kita tidak tahu mana yang tidak diperlukan. Sekali lagi, ini menghambat refactoring.

Juga, keterbacaan tes sangat menderita, ada banyak kode di sana yang tidak kami tulis karena kami ingin, tetapi karena kami harus; bukan kita yang menginginkan kode itu di sana. Tes yang menggunakan benda tiruan terlihat sangat kompleks dan seringkali sulit dibaca. Tes harus membantu pembaca memahami, bagaimana kelas yang diuji harus digunakan, sehingga harus sederhana dan mudah. Jika tidak dapat dibaca, tidak ada yang akan memeliharanya; pada kenyataannya, lebih mudah untuk menghapusnya daripada mempertahankannya.

Bagaimana cara memperbaikinya? Dengan mudah:

  • Coba gunakan kelas nyata daripada mengejek jika memungkinkan. Gunakan yang asli PdfFormatterImpl. Jika tidak memungkinkan, ubah kelas nyata untuk memungkinkannya. Tidak bisa menggunakan kelas dalam tes biasanya menunjukkan beberapa masalah dengan kelas. Memperbaiki masalah adalah situasi win-win - Anda memperbaiki kelas dan Anda memiliki tes yang lebih sederhana. Di sisi lain, tidak memperbaikinya dan menggunakan ejekan adalah situasi yang tidak dapat dimenangkan - Anda tidak memperbaiki kelas yang sebenarnya dan Anda memiliki tes yang lebih kompleks dan kurang dapat dibaca yang menghambat refactoring lebih lanjut.
  • Coba buat implementasi tes sederhana dari antarmuka daripada mengejeknya di setiap tes, dan gunakan kelas tes ini di semua tes Anda. Buat TestPdfFormatteryang tidak melakukan apa-apa. Dengan begitu Anda bisa mengubahnya sekali untuk semua tes dan tes Anda tidak berantakan dengan pengaturan panjang di mana Anda melatih bertopik Anda.

Secara keseluruhan, benda tiruan dapat digunakan, tetapi ketika tidak digunakan dengan hati-hati, benda tiruan itu sering mendorong praktik buruk, menguji detail implementasi, menghalangi refactoring, dan menghasilkan tes yang sulit dibaca dan sulit untuk mempertahankan tes .

Untuk beberapa detail lebih lanjut tentang kekurangan tiruan lihat juga Obyek Mock: Kekurangan dan Kasus Penggunaan .

Jan Soltis
sumber
1
Jawaban yang dipikirkan dengan matang, dan saya sebagian besar setuju. Saya akan mengatakan bahwa karena tes unit adalah pengujian kotak putih, harus mengubah tes ketika Anda mengubah implementasi untuk mengirim PDF yang lebih bagus mungkin bukan beban yang tidak masuk akal. Terkadang mengolok-olok bisa menjadi cara yang bermanfaat untuk menerapkan stub dengan cepat daripada memiliki banyak pelat ketel. Namun dalam praktiknya tampaknya penggunaannya tidak terbatas pada kasus-kasus sederhana ini.
Draemon
1
bukankah inti dari mock adalah bahwa tes Anda konsisten, bahwa Anda tidak perlu khawatir mengejek objek yang implementasinya terus berubah mungkin oleh programmer lain setiap kali Anda menjalankan tes Anda dan Anda mendapatkan hasil tes yang konsisten?
PositiveGuy
1
Poin yang sangat bagus dan relevan (terutama tentang tes kerapuhan). Saya dulu sering menggunakan ejekan ketika saya masih muda, tapi sekarang saya menganggap unit test bahwa eilly bergantung pada ejekan sebagai berpotensi sekali pakai dan lebih fokus pada pengujian integrasi (dengan komponen sebenarnya)
Kemoda
6
"Tidak bisa menggunakan kelas dalam tes biasanya menunjukkan beberapa masalah dengan kelas." Jika kelas adalah layanan (mis. Akses ke database atau proksi ke layanan web), itu harus dianggap sebagai ketergantungan eksternal dan diejek /
dihilangkan
1
Tapi apa yang terjadi kemudian, ketika kita mengubah sendInvitations ()? Jika kode yang diuji dimodifikasi, itu tidak menjamin kontrak sebelumnya lagi, maka harus gagal. Dan biasanya bukan hanya satu tes yang gagal dalam situasi seperti ini . Jika ini masalahnya, kode tersebut tidak bersih diimplementasikan. Verifikasi panggilan metode ketergantungan harus diuji hanya sekali (dalam unit test yang sesuai). Semua kelas lain hanya akan menggunakan contoh tiruan. Jadi saya tidak melihat ada manfaatnya menggabungkan integrasi - dengan unit-tes.
Christopher Will
55

Aturan praktis:

Jika fungsi yang Anda uji memerlukan objek yang rumit sebagai parameter, dan akan merepotkan jika hanya instantiate objek ini (jika, misalnya ia mencoba membuat koneksi TCP), gunakan tiruan.

Orion Edwards
sumber
4

Anda harus mengejek suatu objek ketika Anda memiliki ketergantungan pada unit kode yang Anda coba uji yang perlu "hanya begitu".

Misalnya, ketika Anda mencoba menguji beberapa logika dalam unit kode Anda, tetapi Anda perlu mendapatkan sesuatu dari objek lain dan apa yang dikembalikan dari ketergantungan ini dapat memengaruhi apa yang Anda coba uji - mengejek objek itu.

Podcast hebat tentang topik ini dapat ditemukan di sini

Toran Billups
sumber
Tautan sekarang mengarahkan ke episode saat ini, bukan episode yang dimaksud. Apakah podcast yang dimaksud adalah hanselminutes.com/32/mock-objects ini ?
C Perkins