Unit menguji kelas yang menggunakan DI tanpa menguji internal

12

Saya memiliki kelas yang refactored dalam 1 kelas utama dan 2 kelas lebih kecil. Kelas-kelas utama menggunakan database (seperti banyak kelas saya lakukan) dan mengirim email. Jadi kelas utama memiliki IPersonRepositorydan IEmailRepositorymenyuntikkan yang pada gilirannya mengirim ke 2 kelas yang lebih kecil.

Sekarang saya ingin menguji unit kelas utama, dan telah belajar untuk tidak melepas pekerjaan internal kelas, karena kita harus dapat mengubah kerja internal tanpa melanggar tes unit.

Tetapi karena kelas menggunakan IPersonRepositorydan IEmailRepository, saya HARUS menentukan hasil (tiruan / tiruan) untuk beberapa metode untuk IPersonRepository. Kelas utama menghitung beberapa data berdasarkan data yang ada dan mengembalikannya. Jika saya ingin menguji itu, saya tidak melihat bagaimana saya bisa menulis tes tanpa menentukan bahwa IPersonRepository.GetSavingsByCustomerIdpengembalian x. Tapi kemudian unit test saya 'tahu' tentang cara kerja internal, karena 'tahu' metode mana yang harus diejek dan mana yang tidak.

Bagaimana saya bisa menguji kelas yang telah menyuntikkan dependensi, tanpa tes mengetahui tentang internal?

Latar Belakang:

Dalam pengalaman saya banyak tes seperti ini membuat tiruan untuk repositori dan kemudian memberikan data yang tepat untuk mengejek atau menguji apakah metode tertentu dipanggil selama eksekusi. Either way, tes tahu tentang internal.

Sekarang saya telah melihat presentasi tentang teori (yang saya dengar sebelumnya) bahwa tes tidak boleh tahu tentang implementasinya. Pertama karena Anda tidak menguji cara kerjanya, tetapi juga karena ketika Anda sekarang mengubah implementasi semua tes unit gagal karena mereka 'tahu' tentang implementasi. Sementara saya suka konsep pengujian tidak menyadari implementasi, saya tidak tahu bagaimana mencapainya.

Michel
sumber
1
Segera setelah kelas utama Anda mengharapkan IPersonRepositoryobjek, antarmuka itu dan semua metode yang dijelaskannya tidak "internal" lagi, jadi itu bukan masalah tes. Pertanyaan Anda semestinya adalah "bagaimana saya bisa mengubah kelas menjadi unit yang lebih kecil tanpa mengekspos terlalu banyak di depan umum". Jawabannya adalah "menjaga antarmuka yang ramping" (dengan berpegang teguh pada prinsip pemisahan antarmuka, misalnya). Itulah IMHO titik 2 dalam jawaban @ DavidArno (saya kira tidak perlu bagi saya untuk mengulanginya di jawaban lain).
Doc Brown

Jawaban:

7

Kelas utama menghitung beberapa data berdasarkan data yang ada dan mengembalikannya. Jika saya ingin menguji itu, saya tidak melihat bagaimana saya bisa menulis tes tanpa menentukan bahwa IPersonRepository.GetSavingsByCustomerIdpengembalian x. Tapi kemudian unit test saya 'tahu' tentang cara kerja internal, karena 'tahu' metode mana yang harus diejek dan mana yang tidak.

Anda benar bahwa ini merupakan pelanggaran terhadap prinsip "jangan menguji internal" dan merupakan prinsip umum yang diabaikan orang.

Bagaimana saya bisa menguji kelas yang telah menyuntikkan dependensi, tanpa tes mengetahui tentang internal?

Ada dua solusi yang dapat Anda adopsi untuk mengatasi pelanggaran ini:

1) Memberikan tiruan lengkap IPersonRepository. Pendekatan Anda saat ini dijelaskan adalah untuk memasangkan tiruan ke pekerjaan dalam metode yang diuji dengan hanya mengejek metode yang akan memanggil. Jika Anda memasok tiruan untuk semua metode IPersonRepository, maka Anda menghapus kopling itu. Cara kerja bagian dalam dapat berubah tanpa memengaruhi tiruan, sehingga membuat tes tidak terlalu rapuh.

Pendekatan ini memiliki keuntungan menjaga mekanisme DI tetap sederhana, tetapi dapat membuat banyak pekerjaan jika antarmuka Anda mendefinisikan banyak metode.

2) Jangan menyuntikkan IPersonRepository, menyuntikkan GetSavingsByCustomerIdmetode, atau menyuntikkan nilai tabungan. Masalah dengan menyuntikkan seluruh implementasi antarmuka adalah bahwa Anda kemudian menyuntikkan ("katakan, jangan tanya") sebuah sistem "tanya, jangan katakan", mencampurkan kedua pendekatan. Jika Anda mengambil pendekatan "DI murni", metode harus diberikan (diceritakan) metode yang tepat untuk memanggil jika ingin nilai tabungan, daripada diberikan objek (yang kemudian harus secara efektif meminta metode untuk memanggil).

Keuntungan dari pendekatan ini adalah bahwa ia menghindari perlunya mengejek (di luar metode pengujian yang Anda masukkan ke dalam metode yang diuji). Kerugiannya adalah bahwa hal itu dapat menyebabkan tanda tangan metode berubah sebagai respons terhadap persyaratan perubahan metode.

Kedua pendekatan memiliki pro dan kontra, jadi pilihlah yang paling sesuai dengan kebutuhan Anda.

David Arno
sumber
1
Saya sangat suka ide nomor 2. Saya tidak tahu mengapa saya belum memikirkan itu sebelumnya. Saya telah membuat dependensi pada IRepository selama beberapa tahun sekarang tanpa pernah mempertanyakan diri saya sendiri mengapa saya membuat dependensi pada seluruh IRepository (yang dalam banyak kasus akan mengandung cukup banyak tanda tangan metode) sementara itu hanya memiliki dependensi pada satu atau 2 metode. Jadi, alih-alih menyuntikkan IRepositori, saya bisa menyuntikkan atau meneruskan metode tanda tangan (hanya) yang diperlukan dengan lebih baik.
Michel
1

Pendekatan saya adalah membuat versi 'tiruan' dari repositori yang membaca dari file sederhana yang berisi data yang dibutuhkan.

Ini berarti tes individu tidak 'tahu' tentang pengaturan mock, meskipun jelas proyek tes keseluruhan akan merujuk mock dan memiliki file pengaturan dll.

Ini menghindari pengaturan objek tiruan kompleks yang diperlukan oleh kerangka kerja tiruan dan memungkinkan Anda untuk menggunakan objek 'tiruan' dalam contoh nyata aplikasi Anda untuk pengujian UI dan sejenisnya.

Karena 'tiruan' sepenuhnya diimplementasikan, alih-alih pengaturan khusus untuk skenario pengujian Anda, perubahan implementasi, misalnya katakanlah GetSavingsForCustomer sekarang juga harus menghapus pelanggan. Tidak akan merusak tes (kecuali tentu saja itu benar-benar merusak tes) Anda hanya perlu memperbarui implementasi tiruan tunggal Anda dan semua tes Anda akan berjalan melawannya tanpa mengubah pengaturan mereka

Ewan
sumber
2
Ini masih mengarah pada situasi di mana saya membuat tiruan dengan data untuk metode yang digunakan kelas yang diuji. Jadi saya masih memiliki masalah bahwa ketika implementasi diubah, tes akan rusak.
Michel
1
diedit untuk menjelaskan mengapa pendekatan saya lebih baik, meskipun masih belum sempurna untuk skenario yang saya maksud
Ewan
1

Tes unit biasanya berupa tes papan tulis (Anda memiliki akses ke kode asli). Oleh karena itu tidak apa-apa untuk mengetahui internal sampai batas tertentu, namun untuk pemula lebih mudah untuk tidak, karena Anda tidak boleh menguji perilaku internal (seperti "metode panggilan pertama, lalu b dan kemudian lagi").

Menyuntikkan tiruan yang menyediakan data tidak masalah, karena kelas Anda (unit) tergantung pada data eksternal tersebut (atau penyedia data). Namun, Anda harus memverifikasi hasilnya (bukan cara untuk sampai ke sana)! mis. Anda memberikan contoh Orang dan memverifikasi bahwa email dikirim ke alamat email yang benar, misalnya dengan memberikan ejekan untuk email juga, bahwa ejekan itu tidak melakukan apa-apa selain menyimpan alamat e-mail orang yang dituju untuk kemudian diakses oleh tes Anda -kode. (Saya pikir Martin Fowler menyebut mereka bertopik daripada mengejek, meskipun)

Andy
sumber