Kapan menggunakan antarmuka (pengujian unit, IOC?)

17

Saya curiga saya membuat kesalahan anak sekolah di sini, dan saya sedang mencari klarifikasi. Banyak kelas dalam solusi saya (C #) - berani saya katakan mayoritas - Saya akhirnya menulis antarmuka yang sesuai untuk. Misalnya antarmuka "ICalculator" dan kelas "Kalkulator" yang mengimplementasikannya, meskipun saya tidak akan pernah mungkin mengganti kalkulator itu dengan implementasi yang berbeda. Juga, sebagian besar kelas-kelas ini berada dalam proyek yang sama dengan dependensi mereka - mereka benar-benar hanya perlu internal, tetapi akhirnya menjadi publicefek samping dari implementasi antarmuka masing-masing.

Saya pikir praktik menciptakan antarmuka untuk semuanya ini berakar dari beberapa kepalsuan: -

1) Saya awalnya berpikir bahwa sebuah antarmuka diperlukan untuk membuat unit test mock (saya menggunakan Moq), tetapi sejak itu saya menemukan bahwa sebuah kelas dapat diejek jika anggotanya adalah virtual, dan ia memiliki konstruktor tanpa parameter (koreksi saya jika Aku salah).

2) Saya awalnya berpikir antarmuka diperlukan untuk mendaftarkan kelas dengan kerangka kerja IoC (Castle Windsor), misalnya

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

padahal sebenarnya saya bisa mendaftarkan tipe beton itu sendiri:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) Menggunakan antarmuka, misalnya parameter konstruktor untuk injeksi dependensi, menghasilkan "kopling longgar".

Jadi, apakah saya sudah gila dengan antarmuka ?! Saya mengetahui skenario di mana Anda akan "biasanya" menggunakan antarmuka, misalnya mengekspos API publik, atau untuk hal-hal seperti fungsionalitas "pluggable". Solusi saya memiliki sejumlah kecil kelas yang sesuai dengan kasus penggunaan tersebut, tetapi saya ingin tahu apakah semua antarmuka lain tidak perlu, dan harus dihapus? Mengenai poin 3) di atas, tidakkah saya akan melanggar "kopling longgar" jika saya melakukan ini?

Sunting : - Saya hanya bermain-main dengan Moq, dan tampaknya memerlukan metode untuk publik dan virtual, dan memiliki konstruktor tanpa parameter publik , agar dapat mengejek mereka. Jadi sepertinya saya tidak bisa memiliki kelas internal?

Andrew Stephens
sumber
Saya cukup yakin C # tidak mengharuskan Anda untuk membuat kelas publik untuk mengimplementasikan antarmuka, bahkan jika antarmuka bersifat publik ...
vaughandroid
1
Anda tidak seharusnya menguji anggota pribadi . Ini bisa dilihat sebagai indikator anti-pola kelas dewa. Jika Anda merasa perlu untuk menguji fungsionalitas pribadi maka biasanya ide yang baik untuk merangkum fungsi itu ke dalam kelas itu sendiri.
Spoike
@Seperti proyek yang dimaksud adalah proyek UI, di mana tampak alami untuk membuat semua kelas internal, tetapi mereka masih membutuhkan pengujian unit? Saya sudah membaca di tempat lain tentang tidak perlu menguji metode internal, tetapi saya tidak pernah mengerti alasannya.
Andrew Stephens
Pertama-tama tanyakan pada diri Anda apa unit yang diuji. Apakah seluruh kelas atau metode? Unit itu harus bersifat publik agar unit diuji dengan benar. Dalam proyek UI, saya biasanya memisahkan logika yang dapat diuji menjadi pengontrol / tampilan-model / layanan yang semuanya memiliki metode publik. Tampilan / widget aktual ditransfer ke pengontrol / model tampilan / layanan dan diuji melalui pengujian asap biasa (yaitu memulai aplikasi dan mulai mengklik). Anda dapat memiliki skenario yang harus menguji fungsi yang mendasarinya dan memiliki unit test pada widget mengarah ke tes rapuh dan harus dilakukan dengan hati-hati.
Spoike
@Seperti begitu, apakah Anda membuat metode VM Anda menjadi publik hanya agar dapat diakses oleh unit test kami? Mungkin di situlah saya salah, berusaha keras untuk membuat semuanya internal. Saya berpikir bahwa tangan saya mungkin terikat dengan Moq, yang tampaknya membutuhkan kelas (bukan hanya metode) menjadi publik untuk mengejek mereka (lihat komentar saya pada jawaban Telastyn).
Andrew Stephens

Jawaban:

7

Secara umum, jika Anda membuat antarmuka yang hanya memiliki satu implementor, Anda hanya menulis dua kali dan membuang-buang waktu. Antarmuka saja tidak menyediakan kopling longgar jika dipasangkan dengan erat ke satu implementasi ... Yang mengatakan, jika Anda ingin mengejek hal-hal ini dalam unit test, itu biasanya pertanda bagus bahwa Anda pasti akan membutuhkan lebih dari satu implementor secara nyata kode.

Saya akan menganggapnya sedikit bau kode jika hampir semua kelas Anda memiliki antarmuka sekalipun. Itu berarti mereka hampir semua bekerja dengan satu sama lain dalam satu atau lain cara. Saya menduga bahwa kelas melakukan terlalu banyak (karena tidak ada kelas pembantu) atau Anda sudah terlalu banyak abstrak (oh, saya ingin antarmuka penyedia gravitasi karena itu mungkin berubah!).

Telastyn
sumber
1
Terima kasih balasannya. Seperti disebutkan, ada beberapa alasan yang salah arah mengapa saya melakukan apa yang saya lakukan, meskipun saya tahu bahwa sebagian besar antarmuka tidak akan pernah memiliki lebih dari satu implementasi. Bagian dari masalahnya adalah saya menggunakan kerangka kerja Moq yang bagus untuk mengejek antarmuka, tetapi untuk mengejek kelas harus publik, memiliki parameter publik RKPT, dan metode yang akan diejek harus 'virtual publik').
Andrew Stephens
Sebagian besar kelas yang saya uji unit bersifat internal, dan saya tidak ingin mempublikasikannya hanya untuk membuatnya dapat diuji (walaupun Anda bisa berargumen, untuk itulah saya menulis semua antarmuka ini). Jika saya menyingkirkan antarmuka saya maka saya mungkin perlu menemukan kerangka kerja mengejek baru!
Andrew Stephens
1
@AndrewStephens - tidak semua kebutuhan di dunia diejek. Jika kelas-kelasnya solid (atau digabungkan dengan erat) cukup untuk tidak membutuhkan antarmuka, mereka cukup solid untuk hanya menggunakan apa adanya dalam pengujian unit. Waktu / kerumitan pembuatannya tidak sebanding dengan manfaat pengujian.
Telastyn
-1, karena seringkali Anda tidak tahu berapa banyak implementor yang Anda butuhkan saat pertama kali menulis kode. Jika Anda menggunakan antarmuka dari awal, Anda tidak perlu mengubah klien saat menulis implementor tambahan.
Wilbert
1
@wilbert - tentu saja, antarmuka kecil lebih mudah dibaca daripada kelas, tetapi Anda tidak memilih satu atau yang lain, Anda memiliki kelas atau kelas dan antarmuka. Dan Anda mungkin terlalu banyak membaca jawaban saya. Saya tidak mengatakan tidak pernah membuat antarmuka untuk implementasi tunggal - sebagian besar seharusnya sebenarnya, karena sebagian besar interaksi harus memerlukan fleksibilitas itu. Tapi baca pertanyaannya lagi. Membuat antarmuka untuk semuanya hanyalah kultus kargo. IoC bukan alasan. Mengejek bukanlah alasan. Persyaratan adalah alasan untuk menerapkan fleksibilitas itu.
Telastyn
4

1) Sekalipun kelas konkret dapat diejek, masih mengharuskan Anda membuat anggota virtualdan menyediakan konstruktor tanpa parameter, yang mungkin mengganggu dan tidak diinginkan. Anda (atau anggota tim baru) akan segera menemukan diri Anda secara sistematis menambahkan virtualkonstruktor dan parameterless di setiap kelas baru tanpa berpikir dua kali, hanya karena "begitulah cara kerjanya".

Antarmuka adalah IMO ide yang jauh lebih baik ketika Anda perlu mengejek ketergantungan, karena memungkinkan Anda untuk menunda implementasi sampai ketika Anda benar-benar membutuhkannya, dan membuat kontrak ketergantungan yang jelas dan bagus.

3) Bagaimana itu kepalsuan?

Saya percaya antarmuka sangat bagus untuk mendefinisikan protokol pengiriman pesan antara objek yang berkolaborasi. Mungkin ada kasus ketika kebutuhan mereka masih bisa diperdebatkan, seperti halnya entitas domain misalnya. Namun, di mana pun Anda memiliki mitra yang stabil (yaitu, ketergantungan yang disuntikkan sebagai lawan dari referensi sementara) yang Anda butuhkan untuk berkomunikasi, Anda biasanya harus mempertimbangkan untuk menggunakan antarmuka.

guillaume31
sumber
3

Gagasan IoC adalah membuat jenis beton dapat diganti. Jadi bahkan jika (saat ini) Anda hanya memiliki satu Kalkulator yang mengimplementasikan ICalculator, implementasi kedua hanya dapat bergantung pada antarmuka dan bukan pada detail implementasi dari Kalkulator.

Oleh karena itu, versi pertama pendaftaran IoC-Container Anda adalah yang benar, dan yang harus Anda gunakan di masa depan.

Jika Anda membuat kelas Anda publik atau internal tidak benar-benar terkait, dan begitu juga dengan moq-ing.

Wilbert
sumber
2

Saya benar-benar akan lebih khawatir tentang fakta bahwa Anda tampaknya merasa perlu untuk "mengejek" hampir semua jenis di seluruh proyek Anda.

Tes ganda sebagian besar berguna untuk mengisolasi kode Anda dari dunia eksternal (lib pihak ketiga, disk IO, IO jaringan, dll.) Atau dari perhitungan yang sangat mahal. Menjadi terlalu liberal dengan kerangka isolasi Anda (apakah Anda BENAR-BENAR membutuhkannya? Apakah proyek Anda serumit itu?) Dapat dengan mudah mengarah pada tes yang digabungkan dengan implementasi, dan itu membuat desain Anda lebih kaku. Jika Anda menulis banyak tes yang memverifikasi bahwa beberapa kelas disebut metode X dan Y dalam urutan itu, dan kemudian meneruskan parameter a, b dan c ke metode Z, apakah Anda BENAR-BENAR mendapatkan nilai? Kadang-kadang Anda mendapatkan tes seperti ini saat menguji penebangan atau interaksi dengan lib pihak ketiga, tetapi itu harus menjadi pengecualian, bukan aturannya.

Segera setelah kerangka kerja isolasi Anda memberi tahu Anda bahwa Anda harus membuat barang publik dan bahwa Anda harus selalu memiliki konstruktor tanpa parameter, sekarang saatnya untuk menghindari, karena pada titik ini kerangka Anda memaksa Anda untuk menulis kode yang buruk (lebih buruk).

Berbicara lebih khusus tentang antarmuka, saya menemukan mereka berguna dalam beberapa kasus utama:

  • Ketika Anda perlu mengirim panggilan metode ke berbagai jenis, misalnya ketika Anda memiliki beberapa implementasi (ini adalah yang jelas, karena definisi antarmuka dalam kebanyakan bahasa hanya kumpulan metode virtual)

  • Ketika Anda harus dapat mengisolasi sisa kode Anda dari beberapa kelas. Ini adalah cara normal untuk mengabstraksi sistem file dan akses jaringan serta basis data dan sebagainya dari logika inti aplikasi Anda.

  • Ketika Anda membutuhkan inversi kontrol untuk melindungi batas aplikasi. Ini adalah salah satu cara paling ampuh untuk mengelola dependensi. Kita semua tahu modul tingkat tinggi dan modul tingkat rendah tidak boleh tahu tentang satu sama lain, karena mereka akan berubah karena alasan yang berbeda dan Anda TIDAK ingin memiliki ketergantungan pada HttpContext dalam model domain Anda atau pada beberapa kerangka kerja rendering Pdf di perpustakaan perkalian matriks Anda . Membuat kelas batas Anda mengimplementasikan antarmuka (bahkan jika mereka tidak pernah diganti) membantu melonggarkan kopling antar lapisan, dan secara drastis mengurangi risiko ketergantungan merembes melalui lapisan dari jenis yang mengetahui terlalu banyak jenis lainnya.

sara
sumber
1

Saya condong ke arah gagasan bahwa, jika beberapa logika bersifat pribadi, maka implementasi logika itu seharusnya tidak menjadi perhatian bagi siapa pun, dan karena itu tidak boleh diuji. Jika beberapa logika cukup kompleks sehingga Anda ingin mengujinya, logika itu mungkin memiliki peran yang berbeda dan harus diketahui publik. Misalnya jika saya mengekstrak beberapa kode dalam metode pembantu pribadi, saya menemukan bahwa biasanya itu sesuatu yang bisa juga duduk di kelas publik yang terpisah (formatter, parser, mapper, apa pun).
Adapun antarmuka, saya membiarkan perlunya harus rintisan atau mengejek kelas mendorong saya. Hal-hal seperti repo, saya biasanya ingin dapat rintisan atau mengejek. Seorang mapper dalam tes integrasi, (biasanya) tidak begitu banyak.

Stefan Billiet
sumber