Bagaimana bahasa baru terlihat jika dirancang dari awal agar mudah untuk TDD?

9

Dengan beberapa bahasa yang paling umum (Java, C #, Java, dll) kadang-kadang tampaknya Anda bekerja bertentangan dengan bahasa ketika Anda ingin sepenuhnya TDD kode Anda.

Sebagai contoh, di Java dan C # Anda ingin mengejek dependensi kelas Anda dan sebagian besar kerangka kerja mengejek akan merekomendasikan agar Anda mengejek antarmuka bukan kelas. Ini sering berarti bahwa Anda memiliki banyak antarmuka dengan implementasi tunggal (efek ini bahkan lebih terlihat karena TDD akan memaksa Anda untuk menulis lebih banyak kelas kecil). Solusi yang memungkinkan Anda mengejek kelas beton dengan benar melakukan hal-hal seperti mengubah kompiler atau menimpa pemuat kelas dll, yang cukup buruk.

Jadi, seperti apa sebuah bahasa jika dirancang dari awal untuk menjadi hebat bagi TDD? Mungkin beberapa cara tingkat bahasa cara menggambarkan dependensi (daripada melewati antarmuka ke konstruktor) dan mampu memisahkan antarmuka kelas tanpa melakukannya secara eksplisit?

Geoff
sumber
Bagaimana dengan bahasa yang tidak membutuhkan TDD? blog.8thlight.com/uncle-bob/2011/10/20/Simple-Hickey.html
Job
2
Tidak ada bahasa yang membutuhkan TDD. TDD adalah praktik yang bermanfaat , dan salah satu poin Hickey adalah bahwa hanya karena Anda menguji tidak berarti Anda dapat berhenti berpikir .
Frank Shearar
Pengembangan Berbasis Tes adalah tentang mendapatkan hak API internal dan eksternal Anda , dan melakukannya di muka. Oleh karena itu di Jawa itu adalah semua tentang antarmuka - kelas yang sebenarnya adalah produk sampingan.

Jawaban:

6

Bertahun-tahun yang lalu saya melemparkan prototipe yang membahas pertanyaan serupa; inilah tangkapan layar:

Pengujian Tombol Nol

Idenya adalah bahwa pernyataan sejalan dengan kode itu sendiri, dan semua tes berjalan pada dasarnya pada setiap keystroke. Jadi segera setelah Anda lulus tes, Anda melihat metode berubah menjadi hijau.

Carl Manaster
sumber
2
Haha, itu luar biasa! Saya sebenarnya cukup menyukai ide untuk melakukan tes bersama dengan kode. Ini cukup membosankan (walaupun ada alasan yang sangat bagus) di .NET memiliki rakitan terpisah dengan ruang nama paralel untuk pengujian unit. Ini juga membuat refactoring lebih mudah karena memindahkan kode, secara otomatis memindahkan tes: P
Geoff
Tetapi apakah Anda ingin meninggalkan tes di sana? Apakah Anda membiarkannya diaktifkan untuk kode produksi? Mungkin mereka bisa menjadi # ifdef keluar untuk C, jika tidak kita akan melihat hit ukuran kode / run-time.
Mawg mengatakan mengembalikan Monica
Ini murni prototipe. Jika itu menjadi nyata, maka kita harus mempertimbangkan hal-hal seperti kinerja dan ukuran, tetapi masih terlalu dini untuk mengkhawatirkan hal itu, dan jika kita sampai pada titik itu, tidak akan sulit untuk memilih apa yang harus ditinggalkan. atau, jika diinginkan, untuk meninggalkan pernyataan dari kode yang dikompilasi. Terima kasih atas minat Anda.
Carl Manaster
5

Ini akan diketik secara dinamis daripada diketik secara statis. Mengetik bebek kemudian akan melakukan pekerjaan yang sama seperti antarmuka dalam bahasa yang diketik secara statis. Juga, kelas-kelasnya akan dapat dimodifikasi saat runtime sehingga kerangka kerja pengujian dapat dengan mudah mematikan atau mengolok-olok metode pada kelas yang ada. Ruby adalah salah satu bahasa seperti itu; rspec adalah kerangka uji utama untuk TDD.

Bagaimana pengujian alat bantu pengetikan dinamis

Dengan pengetikan dinamis, Anda dapat membuat objek tiruan dengan hanya membuat kelas yang memiliki antarmuka yang sama (tanda tangan metode) objek kolaborator yang Anda butuhkan untuk mengejek. Misalnya, Anda memiliki beberapa kelas yang mengirim pesan:

class MessageSender
  def send
    # Do something with a side effect
  end
end

Katakanlah kita memiliki MessageSenderUser yang menggunakan instance MessageSender:

class MessageSenderUser

  def initialize(message_sender)
    @message_sender = message_sender
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

Perhatikan penggunaan injeksi ketergantungan di sini , pokok pengujian unit. Kami akan kembali ke sana.

Anda ingin menguji bahwa MessageSenderUser#do_stuffpanggilan mengirim dua kali. Sama seperti yang Anda lakukan dalam bahasa yang diketik secara statis, Anda dapat membuat MessageSender tiruan yang menghitung berapa kali senddipanggil. Tetapi tidak seperti bahasa yang diketik secara statis, Anda tidak perlu kelas antarmuka. Anda tinggal lanjutkan dan buat itu:

class MockMessageSender

  attr_accessor :send_count

  def initialize
    @send_count = 0
  end

  def send
    @send_count += 1
  end

end

Dan gunakan dalam tes Anda:

mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)

Dengan sendirinya, "bebek mengetik" dari bahasa yang diketik secara dinamis tidak menambah banyak pengujian dibandingkan dengan bahasa yang diketik secara statis. Tetapi bagaimana jika kelas tidak ditutup, tetapi dapat dimodifikasi saat runtime? Itu adalah pengubah game. Mari kita lihat caranya.

Bagaimana jika Anda tidak harus menggunakan injeksi ketergantungan untuk membuat kelas dapat diuji?

Misalkan MessageSenderUser hanya akan menggunakan MessageSender untuk mengirim pesan, dan Anda tidak perlu mengizinkan substitusi MessageSender dengan kelas lain. Dalam satu program sering terjadi. Mari kita menulis ulang MessageSenderUser sehingga hanya membuat dan menggunakan MessageSender, tanpa suntikan ketergantungan.

class MessageSenderUser

  def initialize
    @message_sender = MessageSender.new
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

MessageSenderUser sekarang lebih mudah digunakan: Tidak ada yang membuatnya perlu membuat MessageSender agar dapat digunakan. Ini tidak terlihat seperti peningkatan besar dalam contoh sederhana ini, tetapi sekarang bayangkan bahwa MessageSenderUser dibuat di lebih dari satu tempat, atau memiliki tiga dependensi. Sekarang sistem memiliki banyak sekali contoh yang melintas hanya untuk membuat unit test senang, bukan karena itu perlu meningkatkan desain sama sekali.

Kelas terbuka memungkinkan Anda menguji tanpa injeksi ketergantungan

Kerangka uji dalam bahasa dengan pengetikan dinamis dan kelas terbuka dapat membuat TDD cukup bagus. Berikut cuplikan kode dari tes rspec untuk MessageSenderUser:

mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff

Itu seluruh tes. Jika MessageSenderUser#do_stufftidak memanggil MessageSender#sendtepat dua kali, tes ini gagal. Kelas MessageSender yang sebenarnya tidak pernah dipanggil: Kami memberi tahu tes bahwa setiap kali seseorang mencoba membuat MessageSender, mereka seharusnya mendapatkan MessageSender tiruan kita sebagai gantinya. Tidak diperlukan injeksi ketergantungan.

Sangat menyenangkan untuk melakukan begitu banyak tes sederhana. Lebih baik tidak harus menggunakan injeksi ketergantungan kecuali jika itu benar-benar masuk akal untuk desain Anda.

Tapi apa hubungannya ini dengan kelas terbuka? Perhatikan panggilan ke MessageSender.should_receive. Kami tidak mendefinisikan #should_receive ketika kami menulis MessageSender, jadi siapa yang melakukannya? Jawabannya adalah bahwa kerangka kerja pengujian, membuat beberapa modifikasi hati-hati dari kelas sistem, dapat membuatnya muncul karena melalui #should_receive didefinisikan pada setiap objek. Jika Anda berpikir bahwa memodifikasi kelas sistem seperti itu membutuhkan perhatian, Anda benar. Tetapi ini adalah hal yang sempurna untuk apa yang dilakukan oleh perpustakaan tes di sini, dan kelas terbuka memungkinkannya.

Wayne Conrad
sumber
Jawaban bagus! Kalian mulai membicarakan saya kembali ke bahasa-bahasa yang dinamis :) Saya pikir mengetik bebek adalah kuncinya di sini, trik dengan .new juga mungkin dalam bahasa yang diketik secara statis (meskipun akan jauh lebih tidak elegan).
Geoff
3

Jadi, seperti apa sebuah bahasa jika dirancang dari awal untuk menjadi hebat bagi TDD?

'bekerja dengan baik dengan TDD' tentu tidak cukup untuk menggambarkan suatu bahasa, sehingga bisa "terlihat" seperti apa pun. Gangguan, Prolog, C ++, Ruby, Python ... pilihlah.

Selain itu, tidak jelas bahwa mendukung TDD adalah sesuatu yang paling baik ditangani oleh bahasa itu sendiri. Tentu, Anda dapat membuat bahasa tempat setiap fungsi atau metode memiliki tes terkait, dan Anda dapat membangun dukungan untuk menemukan dan menjalankan tes tersebut. Tetapi kerangka pengujian unit sudah menangani bagian penemuan dan eksekusi dengan baik, dan sulit untuk melihat bagaimana menambahkan persyaratan pengujian untuk setiap fungsi secara bersih. Apakah tes juga perlu tes? Atau ada dua kelas fungsi - yang normal yang membutuhkan tes dan fungsi tes yang tidak membutuhkannya? Itu tidak terlihat sangat elegan.

Mungkin lebih baik untuk mendukung TDD dengan alat dan kerangka kerja. Bangun ke dalam IDE. Buat proses pengembangan yang mendorongnya.

Juga, jika Anda mendesain bahasa, ada baiknya berpikir jangka panjang. Ingatlah bahwa TDD hanyalah satu metodologi, dan bukan cara kerja yang disukai semua orang. Mungkin sulit untuk dibayangkan, tetapi mungkin saja cara yang lebih baik akan datang. Sebagai perancang bahasa, apakah Anda ingin orang-orang harus meninggalkan bahasa Anda ketika itu terjadi?

Yang dapat Anda benar-benar katakan untuk menjawab pertanyaan adalah bahwa bahasa seperti itu akan kondusif untuk pengujian. Saya tahu itu tidak banyak membantu, tapi saya pikir masalahnya ada pada pertanyaan.

Caleb
sumber
Setuju, itu pertanyaan yang sangat sulit untuk diungkapkan dengan baik. Saya pikir apa yang saya maksudkan adalah bahwa alat pengujian saat ini untuk bahasa seperti Java / C # merasa seperti sedikit mengganggu dan bahwa beberapa fitur tambahan / bahasa alternatif akan membuat keseluruhan pengalaman lebih elegan (yaitu tidak memiliki antarmuka untuk 90 % dari kelas saya, hanya kelas yang masuk akal dari sudut pandang desain tingkat tinggi).
Geoff
0

Nah, bahasa yang diketik secara dinamis tidak memerlukan antarmuka eksplisit. Lihat Ruby atau PHP, dll.

Di sisi lain, bahasa yang diketik secara statis seperti Java dan C # atau C ++ memberlakukan jenis dan memaksa Anda untuk menulis antarmuka itu.

Yang tidak saya mengerti adalah apa masalah Anda dengan mereka. Antarmuka adalah elemen kunci dari desain dan mereka digunakan di seluruh pola desain dan dalam menghormati prinsip-prinsip SOLID. Saya, misalnya, sering menggunakan antarmuka dalam PHP karena mereka membuat desain eksplisit dan mereka juga menegakkan desain. Di sisi lain di Ruby Anda tidak memiliki cara untuk menegakkan suatu jenis, itu adalah bahasa yang diketik bebek. Tapi tetap saja, Anda harus membayangkan antarmuka di sana dan Anda harus mengabstraksi desain dalam pikiran Anda untuk mengimplementasikannya dengan benar.

Jadi, meskipun pertanyaan Anda mungkin terdengar menarik, itu menyiratkan bahwa Anda memiliki masalah dengan pemahaman atau menerapkan teknik injeksi ketergantungan.

Dan untuk langsung menjawab pertanyaan Anda, Ruby dan PHP memiliki infrastruktur mengejek yang baik, yang dibangun dalam kerangka pengujian unit dan dikirim secara terpisah (lihat Mockery untuk PHP). Dalam beberapa kasus, kerangka kerja ini bahkan memungkinkan Anda untuk melakukan apa yang Anda sarankan, hal-hal seperti mengejek panggilan statis atau inisialisasi objek tanpa menyuntikkan ketergantungan secara eksplisit.

Patkos Csaba
sumber
1
Saya setuju bahwa antarmuka sangat bagus dan elemen desain utama. Namun, dalam kode saya, saya menemukan bahwa 90% dari kelas memiliki antarmuka dan bahwa hanya ada dua implementasi dari antarmuka itu, kelas dan mengejek kelas itu. Meskipun ini secara teknis tepatnya merupakan titik antarmuka, saya tidak dapat menahan perasaan bahwa itu salah.
Geoff
Saya tidak terlalu akrab dengan mengejek di Jawa dan C #, tetapi sejauh yang saya tahu, objek yang diolok-olok meniru objek nyata. Saya sering melakukan injeksi dependensi dengan menggunakan parameter tipe objek dan mengirim tiruan ke metode / kelas sebagai gantinya. Sesuatu seperti function someName (AnotherClass $ object = null) {$ this-> anotherObject = $ objek? : AnotherClass baru; } Ini adalah trik yang sering digunakan untuk menyuntikkan ketergantungan tanpa berasal dari antarmuka.
Patkos Csaba
1
Di sinilah bahasa dinamis memiliki keunggulan dibandingkan bahasa tipe Java / C # sehubungan dengan pertanyaan saya. Tipuan tipikal dari kelas beton sebenarnya akan membuat subkelas kelas, yang berarti konstruktor kelas beton akan dipanggil, yang merupakan sesuatu yang Anda pasti ingin hindari (ada pengecualian, tetapi mereka memiliki masalah sendiri). Mock dinamis hanya memanfaatkan mengetik bebek sehingga tidak ada hubungan antara kelas beton dan mock itu. Saya sering kode dalam Python, tapi itu sebelum hari TDD saya, mungkin sudah waktunya untuk melihat lagi!
Geoff