Apakah ada titik untuk unit test yang mematikan dan mengolok-olok semuanya publik?

59

Ketika melakukan tes unit dengan cara yang "tepat", yaitu mematikan setiap panggilan publik dan mengembalikan nilai atau cemoohan yang telah ditetapkan, saya merasa seperti saya tidak benar-benar menguji apa pun. Saya benar-benar melihat kode saya dan membuat contoh berdasarkan aliran logika melalui metode publik saya. Dan setiap kali implementasinya berubah, saya harus pergi dan mengubah tes-tes itu, sekali lagi, tidak benar-benar merasa bahwa saya menyelesaikan sesuatu yang bermanfaat (baik itu jangka menengah atau panjang). Saya juga melakukan tes integrasi (termasuk jalur yang tidak bahagia) dan saya tidak terlalu keberatan dengan waktu pengujian yang meningkat. Dengan itu, saya merasa seperti saya benar-benar menguji untuk regresi, karena mereka telah menangkap banyak, sementara semua unit tes lakukan menunjukkan kepada saya bahwa implementasi metode publik saya berubah, yang saya sudah tahu.

Unit testing adalah topik yang luas, dan saya merasa seperti saya yang tidak memahami sesuatu di sini. Apa keuntungan yang menentukan dari pengujian unit vs pengujian integrasi (tidak termasuk waktu overhead)?

enthrops
sumber
4
Dua sen saya: Jangan terlalu sering mengolok-olok ( googletesting.blogspot.com/2013/05/…
Juan Mendes
1
Lihat juga "Mengejek adalah Bau Kode"
user949300

Jawaban:

37

Ketika melakukan tes unit dengan cara yang "tepat", yaitu mematikan setiap panggilan publik dan mengembalikan nilai atau cemoohan yang telah ditetapkan, saya merasa seperti saya tidak benar-benar menguji apa pun. Saya benar-benar melihat kode saya dan membuat contoh berdasarkan aliran logika melalui metode publik saya.

Ini kedengarannya seperti metode yang Anda uji perlu beberapa instance kelas lain (yang harus Anda tiru), dan memanggil beberapa metode sendiri.

Jenis kode ini memang sulit untuk unit-test, karena alasan yang Anda uraikan.

Apa yang saya temukan bermanfaat adalah untuk membagi kelas-kelas tersebut menjadi:

  1. Kelas dengan "logika bisnis" yang sebenarnya. Ini menggunakan sedikit atau tidak ada panggilan ke kelas lain dan mudah untuk diuji (nilai di-nilai keluar).
  2. Kelas yang berinteraksi dengan sistem eksternal (file, database, dll.). Ini membungkus sistem eksternal dan menyediakan antarmuka yang nyaman untuk kebutuhan Anda.
  3. Kelas yang "mengikat semuanya"

Maka kelas-kelas dari 1. mudah untuk unit-test, karena mereka hanya menerima nilai dan mengembalikan hasilnya. Dalam kasus yang lebih kompleks, kelas-kelas ini mungkin perlu melakukan panggilan sendiri, tetapi mereka hanya akan memanggil kelas dari 2. (dan tidak secara langsung memanggil misalnya fungsi database), dan kelas-kelas dari 2. mudah untuk diejek (karena mereka hanya buka bagian-bagian dari sistem terbungkus yang Anda butuhkan).

Kelas-kelas dari 2. dan 3. biasanya tidak dapat diuji unit secara bermakna (karena mereka tidak melakukan sesuatu yang berguna pada mereka sendiri, mereka hanya kode "lem"). OTOH, kelas-kelas ini cenderung relatif sederhana (dan sedikit), sehingga kelas-kelas tersebut harus dicakup secara memadai oleh tes integrasi.


Sebuah contoh

Satu kelas

Katakanlah Anda memiliki kelas yang mengambil harga dari database, menerapkan beberapa diskon dan kemudian memperbarui database.

Jika Anda memiliki ini semua dalam satu kelas, Anda harus memanggil fungsi DB, yang sulit untuk diejek. Dalam pseudocode:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Ketiga langkah akan membutuhkan akses DB, jadi banyak mengejek (kompleks), yang kemungkinan akan rusak jika kode atau struktur DB berubah.

Berpisah

Anda terbagi menjadi tiga kelas: Penghitungan Harga, Penentuan Harga, Aplikasi.

PriceCalculation hanya melakukan perhitungan aktual, dan diberikan nilai yang dibutuhkannya. Aplikasi mengikat semuanya:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

Seperti itu:

  • PriceCalculation merangkum "logika bisnis". Mudah untuk diuji karena tidak memanggil apa pun dengan sendirinya.
  • PriceRepository dapat di-pseudo-unit-test dengan mengatur database tiruan dan menguji panggilan baca dan perbarui. Ini memiliki sedikit logika, karenanya sedikit codepath, jadi Anda tidak perlu terlalu banyak tes ini.
  • Aplikasi tidak dapat diuji unit secara bermakna, karena ini adalah kode lem. Namun, itu juga sangat sederhana, jadi pengujian integrasi harus cukup. Jika nantinya aplikasi menjadi terlalu kompleks, Anda dapat keluar lebih banyak kelas "logika bisnis".

Akhirnya, mungkin PriceCalculation harus melakukan panggilan basis data sendiri. Misalnya karena hanya PriceCalculation yang tahu data mana yang dibutuhkan, sehingga tidak dapat diambil terlebih dahulu oleh App. Kemudian Anda bisa memberikan instance PriceRepository (atau kelas repositori lain), yang disesuaikan dengan kebutuhan PriceCalculation. Kelas ini kemudian perlu diejek, tetapi ini akan sederhana, karena antarmuka PriceRepository sederhana, misalnya PriceRepository.getPrice(articleNo, contractType). Yang paling penting, antarmuka PriceRepository mengisolasi PriceCalculation dari database, sehingga perubahan pada skema DB atau organisasi data tidak mungkin untuk mengubah antarmuka, dan karenanya dapat merusak mock.

sleske
sumber
5
Saya pikir saya sendirian tidak melihat titik dalam unit menguji semuanya, terima kasih
enthrops
4
Saya hanya tidak setuju ketika Anda mengatakan kelas tipe 3 sedikit, saya merasa seperti sebagian besar kode saya adalah tipe 3 dan hampir tidak ada logika bisnis. Inilah yang saya maksud: stackoverflow.com/questions/38496185/…
Rodrigo Ruiz
27

Apa keuntungan yang menentukan dari pengujian unit vs pengujian integrasi?

Itu dikotomi yang salah.

Pengujian unit dan pengujian integrasi memiliki dua tujuan yang serupa tetapi berbeda. Tujuan dari pengujian unit adalah untuk memastikan metode Anda bekerja. Secara praktis, unit test memastikan bahwa kode memenuhi kontrak yang digariskan oleh unit test. Ini jelas dalam cara unit test dirancang: mereka secara khusus menyatakan apa yang seharusnya dilakukan oleh kode , dan menyatakan bahwa kode melakukan itu.

Tes integrasi berbeda. Tes integrasi melatih interaksi antar komponen perangkat lunak. Anda dapat memiliki komponen perangkat lunak yang lulus semua tes mereka dan masih gagal tes integrasi karena mereka tidak berinteraksi dengan benar.

Namun, jika ada keuntungan yang menentukan untuk pengujian unit, inilah: pengujian unit jauh lebih mudah untuk dibuat, dan membutuhkan jauh lebih sedikit waktu dan usaha daripada tes integrasi. Ketika digunakan dengan benar, unit test mendorong pengembangan kode "testable", yang berarti hasil akhir akan lebih andal, lebih mudah dipahami, dan lebih mudah untuk dipelihara. Kode yang dapat diuji memiliki karakteristik tertentu, seperti API yang koheren, perilaku berulang, dan mengembalikan hasil yang mudah ditegaskan.

Tes integrasi lebih sulit dan lebih mahal, karena Anda sering membutuhkan ejekan yang rumit, pengaturan yang rumit, dan pernyataan yang sulit. Pada tingkat integrasi sistem tertinggi, bayangkan mencoba mensimulasikan interaksi manusia dalam UI. Seluruh sistem perangkat lunak dikhususkan untuk otomatisasi semacam itu. Dan itu adalah otomatisasi yang kita kejar; pengujian manusia tidak dapat diulang, dan tidak skala seperti pengujian otomatis.

Akhirnya, pengujian integrasi tidak memberikan jaminan tentang cakupan kode. Berapa banyak kombinasi loop kode, kondisi dan cabang yang Anda uji dengan tes integrasi Anda? Apakah kamu benar-benar tahu? Ada alat yang dapat Anda gunakan dengan unit test dan metode yang sedang diuji yang akan memberi tahu Anda berapa banyak cakupan kode yang Anda miliki, dan apa kompleksitas cyclomatic dari kode Anda. Tetapi mereka hanya benar-benar bekerja dengan baik di tingkat metode, di mana unit test hidup.


Jika tes Anda berubah setiap kali Anda refactor, itu masalah yang berbeda. Tes unit seharusnya tentang mendokumentasikan apa yang dilakukan perangkat lunak Anda, membuktikan bahwa ia melakukan itu, dan kemudian membuktikannya melakukannya lagi ketika Anda menolak implementasi yang mendasarinya. Jika API Anda berubah, atau Anda perlu metode Anda untuk berubah sesuai dengan perubahan dalam desain sistem, itulah yang seharusnya terjadi. Jika itu sering terjadi, pertimbangkan untuk menulis tes Anda terlebih dahulu, sebelum Anda menulis kode. Ini akan memaksa Anda untuk memikirkan keseluruhan arsitektur, dan memungkinkan Anda untuk menulis kode dengan API yang sudah ada.

Jika Anda menghabiskan banyak waktu menulis tes unit untuk kode sepele seperti

public string SomeProperty { get; set; }

maka Anda harus menguji kembali pendekatan Anda. Unit testing seharusnya menguji perilaku, dan tidak ada perilaku pada baris kode di atas. Namun, Anda telah membuat ketergantungan pada kode Anda di suatu tempat, karena properti itu hampir pasti akan dirujuk ke tempat lain dalam kode Anda. Alih-alih melakukan itu, pertimbangkan menulis metode yang menerima properti yang dibutuhkan sebagai parameter:

public string SomeMethod(string someProperty);

Sekarang metode Anda tidak memiliki ketergantungan pada sesuatu di luar dirinya sendiri, dan sekarang lebih dapat diuji, karena sepenuhnya mandiri. Memang, Anda tidak akan selalu bisa melakukan ini, tetapi itu memindahkan kode Anda ke arah yang lebih dapat diuji, dan kali ini Anda sedang menulis unit test untuk perilaku aktual.

Robert Harvey
sumber
2
Saya menyadari bahwa pengujian unit dan server pengujian tujuan yang berbeda, namun, saya masih tidak mendapatkan bagaimana tes unit berguna jika Anda mematikan dan mengejek semua panggilan publik yang dibuat unit test. Saya akan mengerti 'kode memenuhi kontrak yang diuraikan oleh tes unit', jika bukan karena bertopik dan mengejek; unit test saya secara harfiah adalah refleksi dari logika di dalam metode yang saya uji. Anda (saya) tidak benar-benar menguji apa pun, hanya melihat kode Anda, dan 'mengubahnya' menjadi tes. Mengenai kesulitan mengotomatisasi dan cakupan kode, saya saat ini sedang melakukan Rails, dan keduanya diurus dengan baik.
enthrops
2
Jika tes Anda hanya refleksi dari logika metode, Anda salah melakukannya. Unit tes Anda pada dasarnya harus menyerahkan metode nilai, menerima nilai kembali, dan membuat pernyataan tentang apa nilai balik itu seharusnya. Tidak diperlukan logika untuk melakukan itu.
Robert Harvey
2
Masuk akal, tetapi masih harus mematikan semua panggilan publik lainnya (db, beberapa 'global', seperti status pengguna saat ini, dll) dan berakhir dengan kode pengujian mengikuti metode logika.
enthrops
1
Jadi saya kira tes unit adalah untuk sebagian besar hal-hal terisolasi semacam itu untuk 'set input -> set hasil yang diharapkan'?
enthrops
1
Pengalaman saya dalam membuat banyak unit dan tes integrasi (belum lagi mengejek canggih, pengujian integrasi, dan alat cakupan kode yang digunakan oleh tes tersebut) bertentangan dengan sebagian besar klaim Anda di sini: 1) "Tujuan pengujian unit adalah untuk memastikan bahwa Anda kode melakukan apa yang seharusnya ": hal yang sama berlaku untuk pengujian integrasi (bahkan lebih lagi); 2) "unit test jauh lebih mudah diatur": tidak, mereka tidak (cukup sering, ini adalah tes integrasi yang lebih mudah); 3) "Ketika digunakan dengan benar, unit test mendorong pengembangan" codeable "code": sama dengan tes integrasi; (lanjutan)
Rogério
4

Tes unit dengan mengejek adalah untuk memastikan implementasi kelas sudah benar. Anda mengejek antarmuka publik dari dependensi kode yang Anda uji. Dengan cara ini Anda memiliki kontrol atas segala sesuatu di luar kelas dan yakin bahwa tes yang gagal adalah karena sesuatu yang internal ke kelas dan bukan di salah satu objek lainnya.

Anda juga menguji perilaku kelas yang diuji, bukan implementasinya. Jika Anda memperbaiki kode (membuat metode internal baru, dll) pengujian unit seharusnya tidak gagal. Tetapi jika Anda mengubah apa yang dilakukan metode publik maka tes pasti gagal karena Anda telah mengubah perilaku.

Sepertinya Anda menulis tes setelah Anda menulis kode, cobalah menulis tes terlebih dahulu. Cobalah menguraikan perilaku yang harus dimiliki kelas dan kemudian menulis jumlah kode minimum untuk membuat tes lulus.

Pengujian unit dan pengujian integrasi berguna untuk memastikan kualitas kode Anda. Tes unit memeriksa setiap komponen secara terpisah. Dan tes integrasi memastikan bahwa semua komponen berinteraksi dengan benar. Saya ingin memiliki kedua jenis di ruang tes saya.

Tes unit telah membantu saya dalam pengembangan saya karena saya dapat fokus pada satu bagian aplikasi sekaligus. Mengejek komponen yang belum saya buat. Mereka juga bagus untuk regresi, karena mereka mendokumentasikan bug dalam logika yang saya temukan (bahkan dalam tes unit).

MEMPERBARUI

Membuat tes yang hanya memastikan bahwa metode yang dipanggil memiliki nilai karena Anda memastikan bahwa metode yang benar-benar dipanggil. Khususnya jika Anda menulis tes Anda terlebih dahulu, Anda memiliki daftar periksa metode yang perlu terjadi. Karena kode ini cukup prosedural, Anda tidak perlu banyak memeriksa selain itu metode dipanggil. Anda melindungi kode untuk perubahan di masa mendatang. Ketika Anda perlu memanggil satu metode sebelum yang lain. Atau metode selalu dipanggil meskipun metode awal melempar pengecualian.

Tes untuk metode ini mungkin tidak pernah berubah atau dapat berubah hanya ketika Anda mengubah metode. Kenapa ini hal yang buruk? Ini membantu memperkuat menggunakan tes. Jika Anda harus memperbaiki tes setelah mengubah kode, Anda akan terbiasa mengubah tes dengan kode.

Schleis
sumber
Dan jika suatu metode tidak menyebut sesuatu yang bersifat pribadi, maka tidak ada gunanya mengujinya, benar?
enthrops
Jika metode ini pribadi, Anda tidak mengujinya secara eksplisit, itu harus diuji melalui antarmuka publik. Semua metode publik harus diuji, untuk memastikan bahwa perilaku itu benar.
Schleis
Tidak, maksud saya jika metode publik tidak menyebut sesuatu yang pribadi, apakah masuk akal untuk menguji metode publik itu?
enthrops
Iya. Metode melakukan sesuatu bukan? Jadi harus diuji. Dari sudut pandang pengujian saya tidak tahu apakah itu menggunakan sesuatu yang pribadi. Saya hanya tahu bahwa jika saya memberikan input A, saya harus mendapatkan output B.
Schleis
Oh ya, metodenya melakukan sesuatu dan sesuatu itu adalah panggilan ke metode publik lainnya (dan hanya itu). Jadi cara Anda akan 'benar' menguji yaitu rintisan panggilan dengan beberapa nilai kembali dan kemudian mengatur harapan pesan. Apa sebenarnya yang Anda uji dalam kasus ini? Bahwa panggilan yang benar dilakukan? Nah, Anda menulis metode itu dan Anda bisa melihatnya dan melihat apa fungsinya. Saya pikir unit testing lebih tepat untuk metode terisolasi yang seharusnya digunakan seperti 'input -> output', sehingga Anda dapat mengatur banyak contoh dan kemudian melakukan pengujian regresi ketika Anda refactor.
enthrops
3

Saya mengalami pertanyaan serupa - sampai saya menemukan kekuatan pengujian komponen. Singkatnya, mereka sama dengan tes unit kecuali bahwa Anda tidak mengejek secara default tetapi menggunakan objek nyata (idealnya melalui injeksi ketergantungan).

Dengan begitu, Anda dapat dengan cepat membuat tes yang kuat dengan cakupan kode yang baik. Tidak perlu memperbarui tiruan Anda sepanjang waktu. Mungkin sedikit kurang tepat daripada tes unit dengan 100% mengejek, tetapi waktu dan uang yang Anda simpan mengkompensasinya. Satu-satunya hal yang Anda benar-benar perlu gunakan untuk mengejek atau perlengkapan adalah penyimpanan backend atau layanan eksternal.

Sebenarnya, mengejek yang berlebihan adalah anti-pola: TDD Anti-Patterns dan Mock adalah kejahatan .

lastzero
sumber
0

Meskipun op sudah menandai jawaban, saya hanya menambahkan 2 sen saya di sini.

Apa keuntungan yang menentukan dari pengujian unit vs pengujian integrasi (tidak termasuk waktu overhead)?

Dan juga dalam menanggapi

Ketika melakukan tes unit dengan cara yang "tepat", yaitu mematikan setiap panggilan publik dan mengembalikan nilai atau cemoohan yang telah ditetapkan, saya merasa seperti saya tidak benar-benar menguji apa pun.

Ada yang membantu tapi tidak persis apa yang ditanyakan OP:

Tes Unit berhasil tetapi masih ada bug?

dari sedikit pengalaman saya pada suite pengujian, saya mengerti bahwa Tes Unit selalu menguji fungsi tingkat metode paling dasar dari sebuah kelas. Menurut pendapat saya, setiap metode baik publik, swasta atau internal layak untuk memiliki unit test khusus. Bahkan dalam pengalaman saya baru-baru ini saya memiliki metode publik yang memanggil metode pribadi kecil lainnya. Jadi, ada dua pendekatan:

  1. jangan membuat unit test untuk metode pribadi.
  2. buat Tes Unit untuk metode pribadi.

Jika Anda berpikir secara logis, tujuan memiliki metode pribadi adalah: metode publik utama menjadi terlalu besar atau berantakan. Untuk mengatasi ini, Anda harus melakukan refactor secara bijaksana dan membuat potongan kode kecil yang layak menjadi metode pribadi terpisah yang pada gilirannya membuat metode publik utama Anda tidak terlalu besar. Anda menolak dengan mengingat bahwa metode pribadi ini dapat digunakan kembali nanti. Mungkin ada kasus di mana tidak ada metode publik lainnya tergantung pada metode pribadi itu, tetapi siapa yang tahu tentang masa depan.


Mempertimbangkan kasus ketika metode pribadi digunakan kembali oleh banyak metode publik lainnya.

Jadi, jika saya memilih pendekatan 1: Saya akan menduplikasi tes Unit dan itu akan menjadi rumit, karena Anda memiliki jumlah Tes Unit untuk setiap cabang metode publik maupun metode pribadi.

Jika saya memilih pendekatan 2: kode yang ditulis untuk unit test akan relatif lebih sedikit, dan akan lebih mudah untuk diuji.


Mempertimbangkan kasus ketika metode pribadi tidak digunakan kembali. Tidak ada gunanya menulis unit test terpisah untuk metode itu.

Adapun seperti Tes Integrasi yang bersangkutan, mereka cenderung lengkap dan lebih tinggi. Mereka akan memberi tahu Anda bahwa dengan diberi input, semua kelas Anda harus sampai pada kesimpulan akhir ini. Untuk memahami lebih lanjut tentang kegunaan pengujian integrasi, silakan lihat tautan yang disebutkan.

tulang kering
sumber