Saya tidak suka menguji fungsionalitas pribadi karena beberapa alasan. Mereka adalah sebagai berikut (ini adalah poin utama untuk orang-orang TLDR):
- Biasanya ketika Anda tergoda untuk menguji metode pribadi kelas, itu bau desain.
- Anda dapat mengujinya melalui antarmuka publik (yang merupakan cara Anda ingin mengujinya, karena itulah cara klien akan memanggil / menggunakannya). Anda bisa mendapatkan rasa aman yang salah dengan melihat lampu hijau pada semua tes yang lulus untuk metode pribadi Anda. Jauh lebih baik / aman untuk menguji kasus tepi pada fungsi pribadi Anda melalui antarmuka publik Anda.
- Anda berisiko mengalami duplikasi tes berat (tes yang terlihat / terasa sangat mirip) dengan menguji metode pribadi. Ini memiliki konsekuensi besar ketika persyaratan berubah, karena lebih banyak tes dari yang diperlukan akan rusak. Itu juga dapat menempatkan Anda pada posisi di mana sulit untuk refactor karena test suite Anda ... yang merupakan ironi utama, karena test suite ada untuk membantu Anda mendesain ulang dan refactor dengan aman!
Saya akan menjelaskan masing-masing dengan contoh nyata. Ternyata 2) dan 3) agak terhubung secara rumit, jadi contoh mereka mirip, meskipun saya menganggap mereka alasan terpisah mengapa Anda tidak boleh menguji metode pribadi.
Ada kalanya menguji metode pribadi sesuai, hanya penting untuk mengetahui kelemahan yang tercantum di atas. Saya akan membahasnya lebih terinci nanti.
Saya juga membahas mengapa TDD bukan alasan yang valid untuk menguji metode pribadi di akhir.
Refactoring jalan keluar dari desain yang buruk
Salah satu pola (anti) yang paling umum yang saya lihat adalah apa yang Michael Feathers sebut sebagai kelas "Iceberg" (jika Anda tidak tahu siapa Michael Feathers, pergi membeli / baca bukunya "Bekerja Efektif dengan Kode Legacy". Dia adalah seseorang yang perlu diketahui jika Anda seorang insinyur / pengembang perangkat lunak profesional). Ada pola (anti) lain yang menyebabkan masalah ini muncul, tetapi sejauh ini yang paling umum yang pernah saya temui. Kelas "Gunung Es" memiliki satu metode publik, dan sisanya adalah privat (itulah sebabnya menggoda untuk menguji metode privat). Ini disebut kelas "Gunung Es" karena biasanya ada satu-satunya metode publik yang muncul, tetapi fungsi lainnya disembunyikan di bawah air dalam bentuk metode pribadi.
Misalnya, Anda mungkin ingin menguji GetNextToken()
dengan menyebutnya pada string secara berturut-turut dan melihat bahwa itu mengembalikan hasil yang diharapkan. Fungsi seperti ini benar-benar memerlukan tes: perilaku itu tidak sepele, terutama jika aturan tokenizing Anda rumit. Mari kita berpura-pura tidak semua yang rumit, dan kita hanya ingin mengikat token dibatasi oleh ruang. Jadi Anda menulis tes, mungkin terlihat seperti ini (beberapa kode aguedostic bahasa, semoga idenya jelas):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Yah, itu sebenarnya terlihat cukup bagus. Kami ingin memastikan kami mempertahankan perilaku ini saat kami membuat perubahan. Tetapi GetNextToken()
adalah fungsi pribadi ! Jadi kami tidak dapat mengujinya seperti ini, karena ia bahkan tidak dapat dikompilasi (dengan asumsi kami menggunakan beberapa bahasa yang benar-benar menegakkan publik / pribadi, tidak seperti beberapa bahasa scripting seperti Python). Tetapi bagaimana dengan mengubah RuleEvaluator
kelas untuk mengikuti Prinsip Tanggung Jawab Tunggal (Single Responsibility Principle)? Sebagai contoh, kita tampaknya memiliki pengurai, tokenizer, dan evaluator macet ke dalam satu kelas. Bukankah lebih baik memisahkan tanggung jawab itu? Selain itu, jika Anda membuat Tokenizer
kelas, maka metode publiknya adalah HasMoreTokens()
danGetNextTokens()
. The RuleEvaluator
kelas bisa memilikiTokenizer
objek sebagai anggota. Sekarang, kita dapat menyimpan tes yang sama seperti di atas, kecuali kita menguji Tokenizer
kelas, bukan RuleEvaluator
kelas.
Berikut ini tampilannya di UML:
Perhatikan bahwa desain baru ini meningkatkan modularitas, sehingga Anda dapat berpotensi menggunakan kembali kelas-kelas ini di bagian lain dari sistem Anda (sebelum Anda tidak bisa, metode pribadi tidak dapat digunakan kembali dengan definisi). Ini adalah keuntungan utama dari mematahkan RuleEvaluator, bersama dengan peningkatan pemahaman / lokalitas.
Tes akan terlihat sangat mirip, kecuali itu sebenarnya akan mengkompilasi kali ini karena GetNextToken()
metode ini sekarang umum di Tokenizer
kelas:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Menguji komponen pribadi melalui antarmuka publik dan menghindari duplikasi pengujian
Bahkan jika Anda tidak berpikir Anda dapat memecah masalah Anda menjadi komponen modular yang lebih sedikit (yang Anda dapat 95% dari waktu jika Anda hanya mencoba untuk melakukannya), Anda dapat dengan mudah menguji fungsi pribadi melalui antarmuka publik. Sering kali anggota pribadi tidak layak diuji karena mereka akan diuji melalui antarmuka publik. Banyak kali yang saya lihat adalah tes yang terlihat sangat mirip, tetapi menguji dua fungsi / metode yang berbeda. Apa yang akhirnya terjadi adalah ketika persyaratan berubah (dan selalu terjadi), Anda sekarang memiliki 2 tes yang rusak alih-alih 1. Dan jika Anda benar-benar menguji semua metode pribadi Anda, Anda mungkin memiliki lebih dari 10 tes yang rusak daripada 1. Singkatnya , Menguji fungsi pribadi (dengan menggunakanFRIEND_TEST
atau menjadikannya publik atau menggunakan refleksi) yang jika tidak dapat diuji melalui antarmuka publik dapat menyebabkan duplikasi pengujian . Anda benar-benar tidak menginginkan ini, karena tidak ada yang lebih menyakitkan daripada test suite Anda yang memperlambat Anda. Seharusnya mengurangi waktu pengembangan dan mengurangi biaya perawatan! Jika Anda menguji metode pribadi yang diuji melalui antarmuka publik, test suite mungkin melakukan sebaliknya, dan secara aktif meningkatkan biaya pemeliharaan dan meningkatkan waktu pengembangan. Saat Anda membuat fungsi pribadi menjadi publik, atau jika Anda menggunakan sesuatu seperti FRIEND_TEST
dan / atau refleksi, Anda biasanya akan menyesalinya dalam jangka panjang.
Pertimbangkan kemungkinan implementasi Tokenizer
kelas berikut:
Katakanlah yang SplitUpByDelimiter()
bertanggung jawab untuk mengembalikan array sehingga setiap elemen dalam array adalah token. Lebih jauh, katakan saja itu GetNextToken()
hanyalah iterator pada vektor ini. Jadi tes publik Anda mungkin terlihat seperti ini:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Mari kita berpura-pura memiliki apa yang disebut Michael Feather sebagai alat meraba - raba . Ini adalah alat yang memungkinkan Anda menyentuh bagian pribadi orang lain. Contohnya adalah FRIEND_TEST
dari googletest, atau refleksi jika bahasa mendukungnya.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Nah, sekarang katakanlah persyaratan berubah, dan tokenizing menjadi jauh lebih kompleks. Anda memutuskan bahwa pembatas string sederhana tidak akan cukup, dan Anda membutuhkan Delimiter
kelas untuk menangani pekerjaan itu. Secara alami, Anda akan mengharapkan satu tes untuk istirahat, tetapi rasa sakit itu meningkat ketika Anda menguji fungsi pribadi.
Kapan pengujian metode pribadi sesuai?
Tidak ada "satu ukuran untuk semua" dalam perangkat lunak. Terkadang tidak apa-apa (dan sebenarnya ideal) untuk "melanggar aturan". Saya sangat menganjurkan tidak menguji fungsionalitas pribadi ketika Anda bisa. Ada dua situasi utama ketika saya pikir tidak apa-apa:
Saya telah bekerja secara luas dengan sistem warisan (itulah sebabnya saya penggemar berat Michael Feathers), dan saya dapat dengan aman mengatakan bahwa kadang-kadang paling aman untuk hanya menguji fungsionalitas pribadi. Ini bisa sangat membantu untuk memasukkan "tes karakterisasi" ke baseline.
Anda sedang terburu-buru, dan harus melakukan hal tercepat untuk di sini dan sekarang. Dalam jangka panjang, Anda tidak ingin menguji metode pribadi. Tetapi saya akan mengatakan bahwa biasanya perlu waktu untuk refactor untuk mengatasi masalah desain. Dan kadang-kadang Anda harus mengirim dalam seminggu. Tidak apa-apa: lakukan yang cepat dan kotor dan uji metode pribadi menggunakan alat meraba jika itu yang Anda pikirkan adalah cara tercepat dan paling dapat diandalkan untuk menyelesaikan pekerjaan. Tetapi pahamilah bahwa apa yang Anda lakukan adalah suboptimal dalam jangka panjang, dan tolong pertimbangkan kembali ke sana (atau, jika dilupakan tetapi Anda melihatnya nanti, perbaiki).
Mungkin ada situasi lain di mana tidak apa-apa. Jika Anda pikir tidak apa-apa, dan Anda memiliki pembenaran yang baik, maka lakukanlah. Tidak ada yang menghentikan Anda. Sadarilah potensi biaya.
Alasan TDD
Selain itu, saya benar-benar tidak suka orang yang menggunakan TDD sebagai alasan untuk menguji metode pribadi. Saya berlatih TDD, dan saya tidak berpikir TDD memaksa Anda untuk melakukan ini. Anda dapat menulis tes Anda (untuk antarmuka publik Anda) terlebih dahulu, dan kemudian menulis kode untuk memenuhi antarmuka itu. Kadang-kadang saya menulis tes untuk antarmuka publik, dan saya akan memuaskannya dengan menulis satu atau dua metode pribadi yang lebih kecil juga (tapi saya tidak menguji metode pribadi secara langsung, tetapi saya tahu mereka berfungsi atau tes publik saya akan gagal ). Jika saya perlu menguji kasus tepi dari metode pribadi itu, saya akan menulis sejumlah besar tes yang akan mengenai mereka melalui antarmuka publik saya.Jika Anda tidak tahu cara menabrak casing tepi, ini adalah pertanda kuat yang Anda butuhkan untuk merefleksikan komponen kecil masing-masing dengan metode publik mereka sendiri. Ini pertanda bahwa fungsi pribadi Anda terlalu banyak, dan di luar ruang kelas .
Juga, kadang-kadang saya menemukan saya menulis tes yang terlalu besar untuk dikunyah saat ini, dan jadi saya pikir "eh saya akan kembali ke tes nanti ketika saya memiliki lebih banyak API untuk bekerja dengan" (saya akan berkomentar dan menyimpannya di pikiran saya). Di sinilah banyak devs yang saya temui kemudian akan mulai menulis tes untuk fungsionalitas pribadi mereka, menggunakan TDD sebagai kambing hitam. Mereka berkata "oh, baik saya perlu tes lain, tetapi untuk menulis tes itu, saya akan memerlukan metode pribadi ini. Oleh karena itu, karena saya tidak dapat menulis kode produksi tanpa menulis tes, saya perlu menulis tes untuk metode pribadi. " Tetapi apa yang benar-benar perlu mereka lakukan adalah refactoring menjadi komponen yang lebih kecil dan dapat digunakan kembali daripada menambahkan / menguji sekelompok metode pribadi ke kelas mereka saat ini.
catatan:
Saya menjawab pertanyaan serupa tentang pengujian metode pribadi menggunakan GoogleTest beberapa saat yang lalu. Saya sebagian besar memodifikasi jawaban itu menjadi lebih banyak bahasa agnostik di sini.
PS Inilah kuliah yang relevan tentang kelas gunung es dan alat meraba oleh Michael Feathers: https://www.youtube.com/watch?v=4cVZvoFGJTU