Apa prinsip desain yang mempromosikan kode yang dapat diuji? (merancang kode yang dapat diuji vs desain mengemudi melalui tes)


54

Sebagian besar proyek yang saya kerjakan mempertimbangkan pengembangan dan pengujian unit dalam isolasi yang membuat pengujian unit menulis di kemudian hari menjadi mimpi buruk. Tujuan saya adalah untuk terus menguji selama fase desain tingkat tinggi dan tingkat rendah itu sendiri.

Saya ingin tahu apakah ada prinsip desain yang didefinisikan dengan baik yang mempromosikan kode yang dapat diuji. Salah satu prinsip yang akhirnya saya pahami adalah Inversi Ketergantungan melalui injeksi Ketergantungan dan Inversi Kontrol.

Saya telah membaca bahwa ada sesuatu yang dikenal sebagai SOLID. Saya ingin mengerti jika mengikuti prinsip SOLID secara tidak langsung menghasilkan kode yang mudah diuji? Jika tidak, adakah prinsip desain yang terdefinisi dengan baik yang mempromosikan kode yang dapat diuji?

Saya sadar bahwa ada sesuatu yang dikenal sebagai Test Driven Development. Meskipun, saya lebih tertarik merancang kode dengan pengujian dalam pikiran selama fase desain itu sendiri daripada mengemudi desain melalui tes. Saya harap ini masuk akal.

Satu lagi pertanyaan yang berkaitan dengan topik ini adalah apakah boleh faktor ulang produk / proyek yang ada dan membuat perubahan pada kode dan desain untuk tujuan dapat menulis kasus uji unit untuk setiap modul?



Terima kasih. Saya baru saja mulai membaca artikel dan itu sudah masuk akal.

1
Ini adalah salah satu pertanyaan wawancara saya ("Bagaimana Anda merancang kode agar mudah diuji unit?"). Ini satu-handidly menunjukkan kepada saya jika mereka memahami pengujian unit, mengejek / mematikan, OOD, dan berpotensi TDD. Sayangnya, jawabannya biasanya seperti "Buat database pengujian".
Chris Pitman

Jawaban:


57

Ya, SOLID adalah cara yang sangat baik untuk merancang kode yang dapat dengan mudah diuji. Sebagai primer pendek:

S - Prinsip Tanggung Jawab Tunggal: Suatu objek harus melakukan tepat satu hal, dan harus menjadi satu-satunya objek dalam basis kode yang melakukan satu hal itu. Misalnya, ambil kelas domain, ucapkan Faktur. Kelas Faktur harus mewakili struktur data dan aturan bisnis faktur seperti yang digunakan dalam sistem. Itu harus menjadi satu-satunya kelas yang mewakili faktur dalam basis kode. Ini dapat dipecah lebih lanjut untuk mengatakan bahwa suatu metode harus memiliki satu tujuan dan harus menjadi satu-satunya metode dalam basis kode yang memenuhi kebutuhan ini.

Dengan mengikuti prinsip ini, Anda meningkatkan testabilitas desain Anda dengan mengurangi jumlah tes yang harus Anda tulis yang menguji fungsionalitas yang sama pada objek yang berbeda, dan Anda juga biasanya berakhir dengan potongan-potongan kecil fungsionalitas yang lebih mudah untuk diuji secara terpisah.

O - Prinsip Terbuka / Tertutup: Kelas harus terbuka untuk ekstensi, tetapi tertutup untuk berubah . Setelah suatu objek ada dan bekerja dengan benar, idealnya tidak perlu kembali ke objek itu untuk membuat perubahan yang menambah fungsionalitas baru. Sebagai gantinya, objek harus diperluas, baik dengan menurunkannya atau dengan memasukkan implementasi dependensi baru atau berbeda ke dalamnya, untuk menyediakan fungsionalitas baru tersebut. Ini menghindari regresi; Anda dapat memperkenalkan fungsionalitas baru kapan dan di mana dibutuhkan, tanpa mengubah perilaku objek seperti yang sudah digunakan di tempat lain.

Dengan mematuhi prinsip ini, Anda biasanya meningkatkan kemampuan kode untuk mentolerir "ejekan", dan Anda juga menghindari keharusan menulis ulang tes untuk mengantisipasi perilaku baru; semua tes yang ada untuk objek masih harus bekerja pada implementasi yang tidak diperpanjang, sementara tes baru untuk fungsionalitas baru menggunakan implementasi yang diperluas juga harus bekerja.

Prinsip Substitusi L - Liskov: Kelas A, bergantung pada kelas B, harus dapat menggunakan X: B tanpa mengetahui perbedaannya. Ini pada dasarnya berarti bahwa apa pun yang Anda gunakan sebagai dependensi harus memiliki perilaku yang sama seperti yang terlihat oleh kelas dependen. Sebagai contoh singkat, katakan Anda memiliki antarmuka IWriter yang mengekspos Write (string), yang diimplementasikan oleh ConsoleWriter. Sekarang Anda harus menulis ke file, jadi Anda membuat FileWriter. Dalam melakukannya, Anda harus memastikan bahwa FileWriter dapat digunakan dengan cara yang sama seperti yang dilakukan ConsoleWriter (artinya satu-satunya cara dependen dapat berinteraksi dengannya adalah dengan memanggil Write (string)), dan dengan demikian informasi tambahan yang mungkin diperlukan FileWriter untuk melakukan itu pekerjaan (seperti jalur dan file untuk menulis) harus disediakan dari tempat lain selain tanggungan.

Ini sangat besar untuk menulis kode yang dapat diuji, karena desain yang sesuai dengan LSP dapat memiliki objek "diejek" diganti dengan benda nyata di setiap titik tanpa mengubah perilaku yang diharapkan, memungkinkan potongan kecil kode diuji dalam isolasi dengan keyakinan bahwa sistem kemudian akan bekerja dengan benda-benda nyata yang terhubung.

Prinsip Pemisahan Antarmuka I: Antarmuka harus memiliki metode sesedikit mungkin untuk menyediakan fungsionalitas peran yang ditentukan oleh antarmuka . Sederhananya, lebih banyak antarmuka yang lebih kecil lebih baik daripada lebih sedikit antarmuka yang lebih besar. Ini karena antarmuka yang besar memiliki lebih banyak alasan untuk berubah, dan menyebabkan lebih banyak perubahan di tempat lain dalam basis kode yang mungkin tidak diperlukan.

Ketaatan pada ISP meningkatkan testabilitas dengan mengurangi kompleksitas sistem yang diuji dan ketergantungan dari SUT tersebut. Jika objek yang Anda uji tergantung pada antarmuka IDoThreeThings yang mengekspos DoOne (), DoTwo () dan DoThree (), Anda harus mengejek objek yang mengimplementasikan ketiga metode bahkan jika objek hanya menggunakan metode DoTwo. Tetapi, jika objek hanya bergantung pada IDoTwo (yang hanya memperlihatkan DoTwo), Anda dapat lebih mudah mengejek objek yang memiliki satu metode itu.

D - Ketergantungan Prinsip Pembalikan: Konkret dan abstraksi tidak boleh bergantung pada konkret lain, tetapi pada abstraksi . Prinsip ini secara langsung menegakkan prinsip longgar kopling. Suatu objek seharusnya tidak perlu tahu apa objek itu; melainkan harus peduli apa objek TIDAK. Jadi, penggunaan antarmuka dan / atau kelas dasar abstrak selalu lebih disukai daripada penggunaan implementasi konkret ketika mendefinisikan properti dan parameter suatu objek atau metode. Itu memungkinkan Anda untuk menukar satu implementasi dengan yang lain tanpa harus mengubah penggunaan (jika Anda juga mengikuti LSP, yang sejalan dengan DIP).

Sekali lagi, ini sangat besar untuk testabilitas, karena memungkinkan Anda, sekali lagi, untuk menyuntikkan implementasi tiruan dari ketergantungan alih-alih implementasi "produksi" ke objek Anda yang sedang diuji, sementara masih menguji objek dalam bentuk persis seperti yang akan ia miliki saat ini. dalam produksi. Ini adalah kunci untuk pengujian unit "secara terpisah".


16

Saya telah membaca bahwa ada sesuatu yang dikenal sebagai SOLID. Saya ingin mengerti jika mengikuti prinsip SOLID secara tidak langsung menghasilkan kode yang mudah diuji?

Jika diterapkan dengan benar, ya. Ada posting blog oleh Jeff menjelaskan prinsip-prinsip SOLID dalam cara yang sangat singkat (podcast yang disebutkan juga layak untuk didengarkan), saya sarankan untuk melihat ke sana jika deskripsi yang lebih panjang membuat Anda bingung.

Dari pengalaman saya, 2 prinsip dari SOLID memainkan peran utama dalam merancang kode yang dapat diuji:

  • Prinsip segregasi antarmuka - Anda harus lebih memilih banyak, antarmuka khusus klien daripada yang lebih sedikit, tujuan umum. Ini berpasangan dengan Prinsip Tanggung Jawab Tunggal dan membantu Anda merancang kelas berorientasi fitur / tugas, yang sebaliknya lebih mudah untuk diuji (dibandingkan dengan yang lebih umum, atau "manajer" dan "konteks" yang sering disalahgunakan ) - lebih sedikit ketergantungan , lebih sedikit kerumitan, lebih jelas, tes yang jelas. Singkatnya, komponen kecil mengarah pada tes sederhana.
  • Prinsip inversi ketergantungan - desain berdasarkan kontrak, bukan oleh implementasi. Ini akan sangat bermanfaat bagi Anda saat menguji objek yang kompleks dan menyadari bahwa Anda tidak memerlukan seluruh grafik dependensi hanya untuk mengaturnya , tetapi Anda dapat dengan mudah mengejek antarmuka dan menyelesaikannya.

Saya percaya keduanya akan sangat membantu Anda ketika merancang untuk testability. Yang tersisa juga memiliki dampak, tetapi saya katakan tidak sebesar.

(...) apakah baik-baik saja untuk memfaktorkan ulang produk / proyek yang ada dan membuat perubahan pada kode dan desain dengan tujuan untuk dapat menulis kasus uji unit untuk setiap modul?

Tanpa tes unit yang ada, itu hanya menempatkan - meminta masalah. Tes unit adalah jaminan Anda bahwa kode Anda berfungsi . Memperkenalkan perubahan penghilangan terlihat segera jika Anda memiliki cakupan tes yang tepat.

Sekarang, jika Anda ingin mengubah kode yang ada untuk menambahkan tes unit , ini memperkenalkan celah di mana Anda belum melakukan tes, tetapi sudah mengubah kode . Secara alami, Anda mungkin tidak memiliki petunjuk tentang perubahan yang Anda lakukan. Ini adalah situasi yang ingin Anda hindari.

Tes unit layak ditulis, bahkan terhadap kode yang sulit untuk diuji. Jika kode Anda berfungsi , tetapi tidak diuji unit, solusi yang sesuai adalah menulis tes untuknya dan kemudian memperkenalkan perubahan. Namun, perhatikan bahwa mengubah kode yang diuji agar lebih mudah diuji adalah sesuatu yang mungkin tidak ingin dihabiskan oleh manajemen Anda (Anda mungkin akan mendengarnya membawa sedikit nilai bisnis).


iaw kohesi tinggi dan kopling rendah
jk.

8

PERTANYAAN PERTAMA ANDA:

SOLID memang cara untuk pergi. Saya menemukan bahwa dua aspek paling penting dari singkatan SOLID, ketika datang ke testability, adalah S (Tanggung Jawab Tunggal) dan D (Injeksi Ketergantungan).

Tanggung Jawab Tunggal : Kelas Anda seharusnya hanya melakukan satu hal, dan satu hal saja. kelas yang membuat file, mem-parsing beberapa input, dan menulisnya ke file sudah melakukan tiga hal. Jika kelas Anda hanya melakukan satu hal, Anda tahu persis apa yang diharapkan, dan merancang uji kasus untuk itu seharusnya cukup mudah.

Dependency Injection (DI): Ini memberi Anda kendali atas lingkungan pengujian. Alih-alih membuat objek forreign di dalam kode Anda, Anda menyuntikkannya melalui konstruktor kelas atau pemanggilan metode. Saat unittesting, Anda cukup mengganti kelas nyata dengan stub atau ejekan, yang Anda kendalikan sepenuhnya.

PERTANYAAN KEDUA ANDA: Idealnya, Anda menulis tes yang mendokumentasikan fungsi kode Anda sebelum refactoring. Dengan cara ini, Anda dapat mendokumentasikan bahwa refactoring Anda mereproduksi hasil yang sama dengan kode aslinya. Namun, masalah Anda adalah bahwa kode yang berfungsi sulit untuk diuji. Ini adalah situasi klasik! Saran saya adalah: Pikirkan baik-baik tentang refactoring sebelum pengujian unit. Jika kamu bisa; tulis tes untuk kode kerja, kemudian refactor kode, dan kemudian refactor tes. Saya tahu ini akan memakan waktu berjam-jam, tetapi Anda akan lebih yakin, bahwa kode yang dire-refraktasi melakukan hal yang sama dengan yang lama. Karena itu, saya sudah menyerah banyak kali. Kelas bisa sangat jelek dan berantakan sehingga menulis ulang adalah satu-satunya cara untuk membuat mereka dapat diuji.


4

Selain jawaban lain, yang berfokus pada pencapaian kopling longgar, saya ingin mengatakan sepatah kata pun tentang pengujian logika yang rumit.

Saya pernah harus menguji unit kelas yang logikanya rumit, dengan banyak persyaratan, dan di mana sulit untuk memahami peran bidang.

Saya mengganti kode ini dengan banyak kelas kecil yang mewakili mesin negara . Logikanya menjadi lebih sederhana untuk diikuti, karena keadaan yang berbeda dari kelas sebelumnya menjadi eksplisit. Setiap kelas negara bebas dari yang lain, sehingga mudah diuji.

Fakta bahwa status eksplisit membuatnya lebih mudah untuk menyebutkan semua kemungkinan jalur kode (transisi status), dan dengan demikian menulis unit-test untuk masing-masing.

Tentu saja, tidak setiap logika kompleks dapat dimodelkan sebagai mesin negara.


3

SOLID adalah awal yang sangat baik, dalam pengalaman saya, empat aspek SOLID benar-benar bekerja dengan baik dengan pengujian unit.

  • Prinsip Tanggung Jawab Tunggal - setiap kelas melakukan satu hal dan satu hal saja. Menghitung nilai, membuka file, mengurai string, apa pun. Jumlah input dan output, serta poin keputusan harus sangat minimal. Yang membuatnya mudah untuk menulis tes.
  • Prinsip substitusi Liskov - Anda harus dapat menggantikan bertopik dan mengolok-olok tanpa mengubah sifat yang diinginkan (hasil yang diharapkan) dari kode Anda.
  • Prinsip pemisahan antarmuka - memisahkan titik kontak dengan antarmuka membuatnya sangat mudah untuk menggunakan kerangka kerja mengejek seperti Moq untuk membuat bertopik dan mengejek. Daripada harus bergantung pada kelas konkret Anda hanya mengandalkan sesuatu yang mengimplementasikan antarmuka.
  • Prinsip Injeksi Ketergantungan - Inilah yang memungkinkan Anda untuk menyuntikkan stub dan ejekan itu ke dalam kode Anda baik melalui konstruktor, properti, atau parameter dalam metode yang ingin Anda uji.

Saya juga akan melihat pola yang berbeda, terutama pola pabrik. Katakanlah Anda memiliki kelas konkret yang mengimplementasikan antarmuka. Anda akan membuat pabrik untuk membuat kelas beton, tapi kembalikan antarmuka.

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

Dalam pengujian Anda, Anda dapat Moq atau kerangka kerja mengejek lainnya untuk menimpa metode virtual dan mengembalikan antarmuka desain Anda. Namun sejauh menyangkut kode implementasi, pabrik belum berubah. Anda juga dapat menyembunyikan banyak detail implementasi Anda dengan cara ini, kode implementasi Anda tidak peduli bagaimana antarmuka dibangun, yang terpenting adalah mendapatkan antarmuka kembali.

Jika Anda ingin sedikit memperluas ini, saya sangat merekomendasikan membaca The Art of Unit Testing . Ini memberikan beberapa contoh hebat tentang bagaimana menggunakan prinsip-prinsip ini, dan itu adalah bacaan yang cukup cepat.


1
Ini disebut prinsip "inversi" ketergantungan, bukan prinsip "injeksi".
Mathias Lykkegaard Lorenzen
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.